Package nexj.core.util

Source Code of nexj.core.util.HTTPClient$AuthenticationStrategy

// Copyright 2010 NexJ Systems Inc. This software is licensed under the terms of the Eclipse Public License 1.0
package nexj.core.util;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.net.URL;
import java.security.AccessController;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivilegedAction;
import java.security.cert.Certificate;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import nexj.core.util.auth.AuthenticationException;
import nexj.core.util.auth.Authenticator;
import nexj.core.util.auth.AuthenticatorFactory;
import nexj.core.util.auth.LoginException;
import nexj.core.util.auth.PasswordAuthenticationProvider;
import nexj.core.version.Version;

/**
* HTTP client implementation.
*/
public class HTTPClient
{
   // constants

   /**
    * Default buffer size.
    */
   public final static int BUF_SIZE = 8192;

   /**
    * Maximum number of authentication/redirection attempts.
    */
   public final static int MAX_REDIR_ATTEMPTS = 16;

   /**
    * Maximum number of basic authentication requests.
    */
   public final static int MAX_BASIC_ATTEMPTS = 3;

   /**
    * SPNEGO with cached credentials.
    */
   public final static int SPNEGO_SILENT = 0;

   /**
    * SPNEGO with explicit credentials.
    */
   public final static int SPNEGO_CRED = 1;

   /**
    * SPNEGO does not work.
    */
   public final static int SPNEGO_NONE = 2;

   /**
    * No authentication has been attempted.
    */
   protected final static int AUTH_NONE = 0;

   /**
    * Basic authentication.
    */
   protected final static int AUTH_BASIC = 1;

   /**
    * SPNEGO authentication.
    */
   protected final static int AUTH_SPNEGO = 2;

   /**
    * HTTPS tunnel not required.
    */
   protected final static int TUNNEL_NONE = 0;

   /**
    * HTTPS tunnel required or being created.
    */
   protected final static int TUNNEL_CREATE = 1;

   /**
    * HTTPS tunnel ready at proxy, but SSL not yet established.
    */
   protected final static int TUNNEL_READY = 2;

   /**
    * HTTP tunnel in-use.
    */
   protected final static int TUNNEL_ESTABLISHED = 3;

   /**
    * Cookie comparator.
    */
   protected final static Comparator COOKIE_COMPARATOR = new Comparator()
   {
      public int compare(Object left, Object right)
      {
         return ((Cookie)right).getPath().length() - ((Cookie)left).getPath().length();
      }
   };

   // attributes

   /**
    * The host URI part.
    */
   protected String m_sHost;

   /**
    * The host:port URI part.
    */
   protected String m_sHostPort;

   /**
    * The HTTP method.
    */
   protected String m_sMethod;

   /**
    * The HTTP protocol.
    */
   protected String m_sProtocol = HTTP.HTTP_1_1;

   /**
    * The HTTP response protocol.
    */
   protected String m_sResponseProtocol;

   /**
    * The HTTP response message.
    */
   protected String m_sResponseMessage;

   /**
    * The HTTP response status code.
    */
   protected int m_nResponseStatus;

   /**
    * The HTTPS tunnel status (one of the TUNNEL_* constants).
    */
   protected int m_nTunnelStatus;

   /**
    * Basic authentication attempt count.
    */
   protected int m_nBasicCount;

   /**
    * The strict SPNEGO flag.
    */
   protected boolean m_bSPNEGOStrict;

   /**
    * True if the authentication handshake has been completed on the client.
    * This does not mean that the authentication is complete,
    * as the server has to send the final response.
    */
   protected boolean m_bAuthDone;

   /**
    * The chunked transfer encoding mode.
    * Chunked transfer encoding has to be enabled explicitly,
    * either through this flag or through the Transfer-Encoding: chunked
    * header, as IIS 6.0 does not understand such requests.
    */
   protected boolean m_bChunked;

   /**
    * True to use a preset proxy.
    */
   protected boolean m_bPresetProxy;

   /**
    * True if m_socket has been retrieved from the cache.
    */
   protected boolean m_bCachedSocket;

   /**
    * The connection timeout in milliseconds (0 means infinite).
    */
   protected int m_nConnectionTimeout = 60000;

   /**
    * The read timeout in milliseconds (0 means infinite).
    */
   protected int m_nReadTimeout;

   /**
    * The token passed in the authentication response header.
    */
   protected String m_sToken;

   /**
    * The connection timestamp.
    */
   protected long m_lTime;

   /**
    * Whether dump and debug level logging should be suppressed.
    */
   protected boolean m_bStealth;

   /**
    * The persistent connection cache timeout in milliseconds.
    */
   protected static long s_lCacheTimeout = 5 * 60 * 1000;

   /**
    * The persistent connection cache limit.
    */
   protected static int s_nCacheLimit = 256;

   // associations

   /**
    * The HTTP proxy.
    */
   protected Proxy m_proxy;

   /**
    * The HTTP server URI.
    */
   protected URI m_uri;

   /**
    * The request header map.
    */
   protected MIMEHeaderMap m_requestHeaderMap = new MIMEHeaderMap();

   /**
    * The response header map.
    */
   protected MIMEHeaderMap m_responseHeaderMap = new MIMEHeaderMap();

   /**
    * The connection socket.
    */
   protected Socket m_socket;

   /**
    * The address of the server.
    */
   protected SocketAddress m_serverAddress;

   /**
    * Server authentication strategy.
    */
   protected AuthenticationStrategy m_serverAuthentication = new AuthenticationStrategy(false);

   /**
    * Proxy authentication strategy.
    */
   protected AuthenticationStrategy m_proxyAuthentication = new AuthenticationStrategy(true);

   /**
    * Authentication strategy for current service.
    */
   protected AuthenticationStrategy m_currentAuthentication = m_serverAuthentication;

   /**
    * The active authenticator.
    */
   protected Authenticator m_authenticator;

   /**
    * The persistent connection key, if any.
    */
   protected ConnectionKey m_key;

   /**
    * Cookie map: Cookie[sDomainKey].
    */
   protected Lookup m_cookieMap;

   /**
    * The cached authentication header.
    */
   protected MIMEHeader m_authHeader;

   /**
    * The cached proxy authentication header.
    */
   protected MIMEHeader m_proxyAuthHeader;

   /**
    * The public certificate of the remote system, when using client certificate
    * authentication. If null, trusts certificates in the default trust store.
    */
   protected Certificate m_trustedCertificate;

   /**
    * The certificate and private key of this system, to be used by client certificate
    * authentication when logging on to a remote system.
    */
   protected KeyStore m_clientCertificateStore;

   /**
    * The password protecting the private key in m_clientCertificateStore.
    */
   protected char[] m_achClientCertificatePassword;

   /**
    * A cache of trust managers for each trusted certificate: (TrustManager[])[Certificate]
    */
   protected static Lookup s_trustedCertificateMap = new SoftHashTab();

   /**
    * A cache of key managers for each client certificate: (KeyManager[])[KeyStore]
    */
   protected static Lookup s_clientCertificateMap = new SoftHashTab();

   /**
    * The SSL socket factory.
    */
   protected static SSLSocketFactory s_sslSocketFactory;

   /**
    * The persistent connection cache: Connection[ConnectionKey].
    */
   protected final static Lookup s_connectionMap = new HashTab();

   /**
    * The persistent connection MRU deque: Connection[].
    */
   protected final static HolderDeque s_connectionDeque = new HashDeque();

   /**
    * Top-level domain set.
    */
   protected final static Holder s_topDomainSet = new HashHolder();

   static
   {
      String[] domains = new String[]{"biz", "com", "edu", "net", "org", "gov", "mil", "int"};

      for (int i = 0; i < domains.length; ++i)
      {
         s_topDomainSet.add(domains[i]);
      }
   }

   /**
    * Set of headers that can be sent to the proxy. The rest of headers will be filtered out.
    */
   protected final static Holder s_proxyHeaderSet = new HashHolder(9); // of type String

   static
   {
      s_proxyHeaderSet.add(HTTP.HEADER_ACCEPT);
      s_proxyHeaderSet.add(HTTP.HEADER_ACCEPT_ENCODING);
      s_proxyHeaderSet.add(HTTP.HEADER_CONTENT_LENGTH);
      s_proxyHeaderSet.add(HTTP.HEADER_COOKIE);
      s_proxyHeaderSet.add(HTTP.HEADER_HOST);
      s_proxyHeaderSet.add(HTTP.HEADER_PROXY_CONNECTION);
      s_proxyHeaderSet.add(HTTP.HEADER_TRANSFER_ENCODING);
      s_proxyHeaderSet.add(HTTP.HEADER_USER_AGENT);
      s_proxyHeaderSet.add(HTTP.PROXY_AUTH_REQUEST_HEADER);
   }

   /**
    * The class logger.
    */
   protected final static Logger s_logger = Logger.getLogger(HTTPClient.class);

   // operations

   /**
    * Sets the connection timeout in milliseconds.
    * @param nTimeout The connection timeout in milliseconds to set (0 means infinite).
    */
   public void setConnectionTimeout(int nTimeout)
   {
      m_nConnectionTimeout = nTimeout;
   }

   /**
    * @return The connection timeout in milliseconds (0 means infinite).
    */
   public int getConnectionTimeout()
   {
      return m_nConnectionTimeout;
   }

   /**
    * Sets the read timeout in milliseconds (0 means infinite).
    * @param nReadTimeout The read timeout in milliseconds (0 means infinite) to set.
    */
   public void setReadTimeout(int nReadTimeout)
   {
      m_nReadTimeout = nReadTimeout;
   }

   /**
    * @return The read timeout in milliseconds (0 means infinite).
    */
   public int getReadTimeout()
   {
      return m_nReadTimeout;
   }

   /**
    * Sets the persistent connection cache limit.
    * @param nCacheLimit The persistent connection cache limit to set.
    */
   public static void setCacheLimit(int nCacheLimit)
   {
      synchronized (s_connectionMap)
      {
         s_nCacheLimit = nCacheLimit;
      }
   }

   /**
    * @return The persistent connection cache limit.
    */
   public static int getCacheLimit()
   {
      synchronized (s_connectionMap)
      {
         return s_nCacheLimit;
      }
   }

   /**
    * Sets the persistent connection cache timeout in milliseconds.
    * @param lCacheTimeout The cache timeout in milliseconds to set.
    */
   public static void setCacheTimeout(long lCacheTimeout)
   {
      synchronized (s_connectionMap)
      {
         s_lCacheTimeout = lCacheTimeout;
      }
   }

   /**
    * @return The persistent connection cache timeout in milliseconds.
    */
   public static long getCacheTimeout()
   {
      synchronized (s_connectionMap)
      {
         return s_lCacheTimeout;
      }
   }

   /**
    * Sets the HTTP proxy.
    * @param proxy The HTTP proxy to set.
    */
   public void setProxy(Proxy proxy)
   {
      if (!ObjUtil.equal(proxy, m_proxy))
      {
         m_proxyAuthHeader = null;
         m_proxyAuthentication.clearServiceName();
      }

      m_proxy = proxy;
      m_bPresetProxy = (proxy != null);
   }

   /**
    * @return The HTTP proxy.
    */
   public Proxy getProxy()
   {
      return m_proxy;
   }

   /**
    * @return The server URI.
    */
   public URI getURI()
   {
      return m_uri;
   }

   /**
    * @return The host URI part.
    */
   public String getHost()
   {
      return m_sHost;
   }

   /**
    * @return The host:port URI part.
    */
   public String getHostPort()
   {
      return m_sHostPort;
   }

   /**
    * @return The HTTP method.
    */
   public String getMethod()
   {
      return m_sMethod;
   }

   /**
    * Sets the HTTP protocol.
    * @param sProtocol The HTTP protocol, one of the HTTP.HTTP_* constants.
    */
   public void setProtocol(String sProtocol)
   {
      assert sProtocol != null;

      m_sProtocol = sProtocol;
   }

   /**
    * Gets the HTTP protocol.
    * @return The HTTP protocol, one of the HTTP.HTTP_* constants.
    */
   public String getProtocol()
   {
      return m_sProtocol;
   }

   /**
    * Sets the password provider.
    * @param passwordProvider The password provider to set.
    */
   public void setPasswordProvider(PasswordAuthenticationProvider passwordProvider)
   {
      m_serverAuthentication.setProvider(passwordProvider);
   }

   /**
    * @return The password provider.
    */
   public PasswordAuthenticationProvider getPasswordProvider()
   {
      return m_serverAuthentication.getProvider();
   }

   /**
    * Sets the password provider for authenticating to the proxy.
    * @param passwordProvider The password provider to set.
    */
   public void setProxyPasswordProvider(PasswordAuthenticationProvider passwordProvider)
   {
      if (!ObjUtil.equal(passwordProvider, m_proxyAuthentication.getProvider()))
      {
         m_proxyAuthHeader = null;
      }

      m_proxyAuthentication.setProvider(passwordProvider);
   }

   /**
    * Gets the password provider for authenticating to the proxy.
    * @return The password provider.
    */
   public PasswordAuthenticationProvider getProxyPasswordProvider()
   {
      return m_proxyAuthentication.getProvider();
   }

   /**
    * Sets the SPNEGO protocol mode (one of the SPNEGO_* constants).
    * @param nSPNEGO The SPNEGO protocol mode (one of the SPNEGO_* constants) to set.
    */
   public void setSPNEGOMode(int nSPNEGO)
   {
      m_serverAuthentication.setSPNEGO(nSPNEGO);
   }

   /**
    * @return The SPNEGO protocol mode (one of the SPNEGO_* constants).
    */
   public int getSPNEGOMode()
   {
      return m_serverAuthentication.getSPNEGO();
   }

   /**
    * Sets the SPNEGO protocol mode for proxy authentication.
    * @param nSPNEGO The SPNEGO protocol mode (one of the SPNEGO_* constants) to set.
    */
   public void setProxySPNEGOMode(int nSPNEGO)
   {
      if (nSPNEGO != m_proxyAuthentication.getSPNEGO())
      {
         m_proxyAuthHeader = null;
      }

      m_proxyAuthentication.setSPNEGO(nSPNEGO);
   }

   /**
    * @return The SPNEGO protocol mode (one of the SPNEGO_* constants).
    */
   public int getProxySPNEGOMode()
   {
      return m_proxyAuthentication.getSPNEGO();
   }

   /**
    * Sets the strict SPNEGO flag.
    * @param bSPNEGOStrict The strict SPNEGO flag to set.
    */
   public void setSPNEGOStrict(boolean bSPNEGOStrict)
   {
      m_bSPNEGOStrict = bSPNEGOStrict;
   }

   /**
    * @return The strict SPNEGO flag.
    */
   public boolean isSPNEGOStrict()
   {
      return m_bSPNEGOStrict;
   }

   /**
    * Sets the chunked transfer encoding mode.
    * @param bChunked The chunked transfer encoding mode to set.
    */
   public void setChunked(boolean bChunked)
   {
      m_bChunked = bChunked;
   }

   /**
    * @return The chunked transfer encoding mode.
    */
   public boolean isChunked()
   {
      return m_bChunked;
   }

   /**
    * Sets the whether dump and debug level logging should be suppressed.
    * @param bStealth Whether dump and debug level logging should be suppressed.
    */
   public void setStealth(boolean bStealth)
   {
      m_bStealth = bStealth;
   }

   /**
    * @return The whether dump and debug level logging should be suppressed.
    */
   public boolean isStealth()
   {
      return m_bStealth;
   }

   /**
    * @return The request header map.
    */
   public MIMEHeaderMap getRequestHeaders()
   {
      return m_requestHeaderMap;
   }

   /**
    * @return The HTTP response status, typically one of the HTTP.STATUS_* constants.
    */
   public int getResponseStatus()
   {
      return m_nResponseStatus;
   }

   /**
    * @return The response protocol, typically one of the HTTP.HTTP_* constants.
    */
   public String getResponseProtocol()
   {
      return m_sResponseProtocol;
   }

   /**
    * @return The response message.
    */
   public String getResponseMessage()
   {
      return m_sResponseMessage;
   }

   /**
    * @return The response header map.
    */
   public MIMEHeaderMap getResponseHeaders()
   {
      return m_responseHeaderMap;
   }

   /**
    * Sets the remote system certificate to trust when using client certificate authentication.
    *
    * @param trustedCertificate The public certificate of the remote system; null to trust
    * certificates in the default trust store.
    */
   public void setTrustedCertificate(Certificate trustedCertificate)
   {
      m_trustedCertificate = trustedCertificate;
   }

   /**
    * Sets the certificate and private key for this system when logging in to a remote system
    * using client certificate authentication.
    *
    * @param clientCertificateStore The certificate and private key.
    * @param sKeyPass The password of the KeyStore.
    */
   public void setClientCertificate(KeyStore clientCertificateStore, String sKeyPass)
   {
      m_clientCertificateStore = clientCertificateStore;
      m_achClientCertificatePassword = sKeyPass.toCharArray();
   }

   /**
    * Invokes an HTTP method on a server.
    * @param uri The HTTP server URL.
    * @param sMethod The method name.
    * @param requestHandler The request handler. Can be null.
    * @param responseHandler The response handler. Can be null.
    * @return The response handler return value.
    * @throws IOException if an I/O error occurs.
    */
   public Object invoke(URI uri, String sMethod, RequestHandler requestHandler, ResponseHandler responseHandler) throws IOException
   {
      assert sMethod != null;

      m_serverAuthentication.clearServiceName();

      if (m_authHeader != null)
      {
         if (uri.equals(m_uri))
         {
            if (m_requestHeaderMap.find(m_authHeader.getName()) == null)
            {
               m_requestHeaderMap.add(m_authHeader);
            }
         }
         else
         {
            m_authHeader = null;
         }
      }

      if (m_proxyAuthHeader != null)
      {
         if (m_requestHeaderMap.find(m_proxyAuthHeader.getName()) == null)
         {
            m_requestHeaderMap.add(m_proxyAuthHeader);
         }
      }

      if (m_proxy == null && !m_bPresetProxy)
      {
         m_proxy = (Proxy)ProxySelector.getDefault().select(uri).get(0);
      }

      m_nBasicCount = -1;
      m_uri = uri;
      m_sMethod = sMethod;

      m_nTunnelStatus = (isHTTPProxy() && HTTP.SCHEME_SSL.equalsIgnoreCase(uri.getScheme())) ? TUNNEL_CREATE : TUNNEL_NONE;

      ByteArrayOutputStream requestStream = null;
      boolean bRequestStream = false;
      boolean bRequestEmpty = false;
      boolean bRequestLength = false;
      long lRequestLength = -1;

      try
      {
      attempt:
         for (int nAttempt = 0; nAttempt < MAX_REDIR_ATTEMPTS; ++nAttempt)
         {
            // Write the request

            connect();

            if (!bRequestStream)
            {
               bRequestEmpty = isRequestEmpty() || m_nTunnelStatus == TUNNEL_CREATE;  // "CONNECT" request should be empty

               if (!bRequestEmpty)
               {
                  bRequestLength = isLengthRequired();

                  if (bRequestLength)
                  {
                     if (requestHandler != null)
                     {
                        requestStream = new ByteArrayOutputStream(1024);
                        requestHandler.handleRequest(this, requestStream);
                     }

                     bRequestStream = true;
                  }
               }
            }

            m_requestHeaderMap.set(HTTP.HEADER_HOST, m_sHostPort);
            m_requestHeaderMap.setDefault(HTTP.HEADER_ACCEPT, "*/*");
            m_requestHeaderMap.setDefault(HTTP.HEADER_ACCEPT_ENCODING, "gzip, deflate");
            m_requestHeaderMap.setDefault(HTTP.HEADER_USER_AGENT, SysUtil.CAPTION + '/' + Version.RELEASE);

            // Avoid blind relay problem with persistent conns. Supports persistent conn to proxy in HTTP/1.1, where persistent is default.
            if (m_proxy == null || m_proxy.type() != Proxy.Type.HTTP || m_nTunnelStatus == TUNNEL_ESTABLISHED)
            {
               if (m_requestHeaderMap.setDefault(HTTP.HEADER_CONNECTION, "keep-alive").findValue("keep-alive") != null)
               {
                  m_requestHeaderMap.setDefault(HTTP.HEADER_KEEP_ALIVE, "300");
               }

               m_requestHeaderMap.remove(HTTP.HEADER_PROXY_CONNECTION);
            }
            else if (m_proxy.type() == Proxy.Type.HTTP)
            {
               // Needed for Squid, not necessary on Microsoft ISA
               // Use non-standard Proxy-Connection header to prevent connection close during Negotiate authentication.
               m_requestHeaderMap.setDefault(HTTP.HEADER_PROXY_CONNECTION, "keep-alive");

               // Avoid blind relay problem
               m_requestHeaderMap.remove(HTTP.HEADER_CONNECTION);
            }

            addCookies(m_requestHeaderMap);

            if (bRequestLength)
            {
               lRequestLength = (requestStream == null) ? 0 : requestStream.size();
               m_requestHeaderMap.set(HTTP.HEADER_CONTENT_LENGTH, Long.toString(lRequestLength));
            }
            else if (bRequestEmpty)
            {
               m_requestHeaderMap.remove(HTTP.HEADER_CONTENT_LENGTH);
            }
            else
            {
               MIMEHeader header = m_requestHeaderMap.find(HTTP.HEADER_CONTENT_LENGTH);

               if (header != null && header.getValue() != null)
               {
                  lRequestLength = Long.parseLong(header.getValue());
               }
            }

            HTTPOutputStream ostream = new HTTPOutputStream(m_socket.getOutputStream());
            boolean bChunked = false;

            if (bRequestEmpty || lRequestLength >= 0)
            {
               m_requestHeaderMap.remove(HTTP.HEADER_TRANSFER_ENCODING);
            }
            else if (isProtocolCurrent(m_sProtocol))
            {
               m_requestHeaderMap.set(HTTP.HEADER_TRANSFER_ENCODING, "chunked");
               bChunked = true;
            }

            if (s_logger.isDebugEnabled() && !m_bStealth)
            {
               s_logger.debug(
                  (
                     (m_nTunnelStatus == TUNNEL_CREATE) ?
                     ("HTTP request: CONNECT " + m_sHostPort) : ("HTTP request: " + m_sMethod + ' ' + m_uri)
                  ) + ' ' + ((m_sProtocol == null) ? "" : m_sProtocol) +
                  ((s_logger.isDumpEnabled()) ? SysUtil.LINE_SEP + m_requestHeaderMap : "")
               );
            }

            try
            {
               if (m_nTunnelStatus == TUNNEL_CREATE)
               {
                  startRequest(ostream, true);
                  ostream.flush();
               }
               else
               {
                  startRequest(ostream, false);

                  ostream.setChunked(bChunked);

                  if (bRequestStream)
                  {
                     if (requestStream != null)
                     {
                        requestStream.writeTo(ostream);
                     }
                  }
                  else
                  {
                     if (requestHandler != null)
                     {
                        if (bRequestEmpty)
                        {
                           requestHandler.handleRequest(this, null);
                        }
                        else
                        {
                           requestStream = new ByteArrayOutputStream(1024);

                           BackupOutputStream bstream = new BackupOutputStream(ostream, requestStream);

                           requestHandler.handleRequest(this, bstream);
                        }
                     }

                     bRequestStream = true;
                  }

                  ostream.close();
               }
            }
            catch (SocketException e)
            {
               if (s_logger.isDebugEnabled())
               {
                  if (s_logger.isDumpEnabled())
                  {
                     s_logger.dump("Output error; retrying.", e);
                  }
                  else
                  {
                     s_logger.debug("Output error; retrying: " + e.getClass().getName() + ": " + e.getMessage());
                  }
               }

               if (m_bCachedSocket)
               {
                  --nAttempt;
               }

               disconnect(false);

               continue;
            }

            // Close the socket output stream, if necessary

            if (m_sProtocol == null)
            {
               m_socket.shutdownOutput();
            }

            // Read the response

            HTTPInputStream istream = new HTTPInputStream(new BufferedInputStream(m_socket.getInputStream(), BUF_SIZE));

            for (;;)
            {
               if (!istream.start())
               {
                  s_logger.debug("Server has disconnected; retrying.");

                  if (m_bCachedSocket)
                  {
                     --nAttempt;
                  }

                  disconnect(false);
                  m_nTunnelStatus = (isHTTPProxy() && HTTP.SCHEME_SSL.equalsIgnoreCase(uri.getScheme())) ? TUNNEL_CREATE : TUNNEL_NONE;
                  continue attempt;
               }

               if (s_logger.isDebugEnabled() && !m_bStealth)
               {
                  s_logger.debug("HTTP response: " + ((m_sResponseProtocol == null) ? "" :
                     m_sResponseProtocol + ' ' + m_nResponseStatus + ' ' + m_sResponseMessage) +
                     ((s_logger.isDumpEnabled()) ? SysUtil.LINE_SEP + m_responseHeaderMap : ""));
               }

               MIMEHeader header = m_responseHeaderMap.find(HTTP.HEADER_SET_COOKIE);

               if (header != null)
               {
                  setCookies(header);
               }

               if (skip())
               {
                  istream.close();
               }
               else
               {
                  break;
               }
            }

            boolean bAuthComplete = authenticate();

            if (bAuthComplete && !redirect() && m_nTunnelStatus != TUNNEL_CREATE)
            {
               Object result = null;

               if (responseHandler != null)
               {
                  InputStream responseStream = istream;
                  MIMEHeader header = m_responseHeaderMap.find(HTTP.HEADER_CONTENT_ENCODING);

                  if (header != null)
                  {
                     int nLast = header.getValueCount() - 1;

                     if (nLast >= 0)
                     {
                        String sCoding = header.getValue(nLast).getName();

                        if (sCoding.equalsIgnoreCase("gzip") || sCoding.equalsIgnoreCase("x-gzip"))
                        {
                           responseStream = new GZIPInputStream(istream);
                        }
                        else if (sCoding.equalsIgnoreCase("deflate"))
                        {
                           responseStream = new InflaterInputStream(istream);
                        }

                        if (responseStream != istream)
                        {
                           if (nLast == 0)
                           {
                              m_responseHeaderMap.remove(HTTP.HEADER_CONTENT_ENCODING);
                           }
                           else
                           {
                              header.removeValue(nLast);
                           }
                        }
                     }
                  }

                  result = responseHandler.handleResponse(this, responseStream);
               }

               complete(istream);

               return result;
            }
            else if (m_nTunnelStatus != TUNNEL_CREATE)
            {
               complete(istream);
            }

            if (m_nTunnelStatus == TUNNEL_CREATE)
            {
               if (bAuthComplete)
               {
                  if (m_nResponseStatus / 100 != 2)
                  {
                     disconnect(false);

                     throw new HTTPClientException("Unable to establish SSL tunnel");
                  }

                  resetAuth();
                  m_nTunnelStatus = TUNNEL_READY;
                  m_proxyAuthHeader = null;
               }
               else
               {
                  complete(istream);
               }
            }
         }

         throw new HTTPClientException("Too many redirects");
      }
      catch (IOException e)
      {
         disconnect(false);

         throw e;
      }
      catch (RuntimeException e)
      {
         disconnect(false);

         throw e;
      }
      finally
      {
         resetAuth();
         m_currentAuthentication.clearUserPassword();

         if (m_authenticator != null)
         {
            m_authenticator.dispose();
            m_authenticator = null;
         }

         m_requestHeaderMap.clear();
         m_responseHeaderMap.clear();

         disconnect(true);
      }
   }

   /**
    * Resets the internal state.
    */
   public void reset()
   {
      m_nResponseStatus = 0;
      m_sResponseMessage = null;
      m_sResponseProtocol = null;
      m_cookieMap = null;
      m_authHeader = null;
      m_proxyAuthHeader = null;
      m_serverAuthentication.clearUserPasswordSaved();
      m_proxyAuthentication.clearUserPasswordSaved();
   }

   /**
    * Set the client cookies from a response header.
    * @param The Set-Cookie header.
    */
   protected void setCookies(MIMEHeader header)
   {
      long lTime = 0;

      for (int i = 0, n = header.getValueCount(); i < n; ++i)
      {
         MIMEHeader.Value value = header.getValue(i);
         String sName = value.getName();
         int k = sName.indexOf('=');

         if (k <= 0)
         {
            continue;
         }

         String sValue = sName.substring(k + 1);

         sName = sName.substring(0, k);

         Cookie cookie = new Cookie(sName, sValue);

         try
         {
            sValue = value.findArg("expires");

            if (sValue != null)
            {
               cookie.setExpiration(HTTP.parseCookieDateTime(sValue).getTime());
            }
            else
            {
               sValue = value.findArg("max-age");

               if (sValue != null)
               {
                  long lMaxAge = Long.parseLong(sValue);

                  if (lMaxAge > 0)
                  {
                     if (lTime == 0)
                     {
                        lTime = System.currentTimeMillis();
                     }

                     if (lMaxAge >= (Long.MAX_VALUE - lTime) / 1000)
                     {
                        lMaxAge = Long.MAX_VALUE;
                     }
                     else
                     {
                        lMaxAge = lMaxAge * 1000 + lTime;
                     }
                  }

                  cookie.setExpiration(lMaxAge);
               }
               else
               {
                  cookie.setExpiration(Long.MIN_VALUE);
               }
            }
         }
         catch (Exception e)
         {
            continue;
         }

         String sDomain = value.findArg("domain");

         if (sDomain == null)
         {
            sDomain = m_sHost.toLowerCase(Locale.ENGLISH);
         }
         else
         {
            sDomain = sDomain.toLowerCase(Locale.ENGLISH);

            if (sDomain.length() == 0)
            {
               continue;
            }

            if (sDomain.charAt(0) != '.')
            {
               sDomain = '.' + sDomain;
            }

            k = sDomain.lastIndexOf('.');

            int m = sDomain.lastIndexOf('.', k - 1);

            if (m < 0)
            {
               continue;
            }

            if (sDomain.lastIndexOf('.', m - 1) < 0 &&
               !s_topDomainSet.contains(sDomain.substring(k + 1)))
            {
               continue;
            }
         }

         cookie.setDomain(sDomain);

         String sPath = value.findArg("path");

         if (sPath == null)
         {
            sPath = m_uri.getRawPath();

            if (sPath == null)
            {
               sPath = "/";
            }
         }

         while (sPath.length() > 1 && sPath.charAt(sPath.length() - 1) == '/')
         {
            sPath = sPath.substring(0, sPath.length() - 1);
         }

         cookie.setPath(sPath);
         cookie.setSecure(value.findArg("secure") != null);

         if (!cookie.matches(m_sHost, m_uri))
         {
            if (s_logger.isDebugEnabled())
            {
               s_logger.debug("Rejecting an invalid cookie: " + cookie);
            }

            continue;
         }

         if (m_cookieMap == null)
         {
            m_cookieMap = new HashTab(4);
         }

         sName = Cookie.getDomainKey(sDomain);

         Cookie lastCookie = (Cookie)m_cookieMap.get(sName);

         if (lastCookie != null)
         {
            Cookie match = lastCookie.find(cookie);

            if (match != null)
            {
               if (match.remove())
               {
                  m_cookieMap.remove(sName);
                  lastCookie = null;
               }
               else if (match == lastCookie)
               {
                  lastCookie = match.getNext();
                  m_cookieMap.put(sName, lastCookie);
               }
            }
         }

         if (cookie.getExpiration() != Long.MIN_VALUE)
         {
            if (lTime == 0)
            {
               lTime = System.currentTimeMillis();
            }

            if (cookie.getExpiration() <= lTime)
            {
               if (s_logger.isDebugEnabled())
               {
                  s_logger.debug("Removing an expired cookie: " + cookie);
               }

               continue;
            }
         }

         if (s_logger.isDebugEnabled())
         {
            s_logger.debug("Adding a cookie: " + cookie);
         }

         m_cookieMap.put(sName, cookie);

         if (lastCookie != null)
         {
            lastCookie.add(cookie);
         }
      }
   }

   /**
    * Adds the client cookies to the request header map.
    * @param headerMap The header map.
    */
   protected void addCookies(MIMEHeaderMap headerMap)
   {
      if (m_cookieMap != null && m_cookieMap.size() != 0)
      {
         String sKey = Cookie.getDomainKey(m_sHost);
         Cookie start = (Cookie)m_cookieMap.get(sKey);

         if (start != null)
         {
            List cookieList = new ArrayList(4);
            Cookie cookie = start;

            do
            {
               if (cookie.getExpiration() != Long.MIN_VALUE &&
                  cookie.getExpiration() <= m_lTime)
               {
                  if (s_logger.isDebugEnabled())
                  {
                     s_logger.debug("Removing an expired cookie: " + cookie);
                  }

                  if (cookie.remove())
                  {
                     m_cookieMap.remove(sKey);

                     break;
                  }

                  if (cookie == start)
                  {
                     start = cookie.getNext();
                     m_cookieMap.put(sKey, start);

                     continue;
                  }
               }
               else
               {
                  if (cookie.matches(m_sHost, m_uri))
                  {
                     cookieList.add(cookie);
                  }
               }

               cookie = cookie.getNext();
            }
            while (cookie != start);

            if (cookieList.size() != 0)
            {
               Collections.sort(cookieList, COOKIE_COMPARATOR);

               StringBuilder buf = new StringBuilder(64);

               for (int i = 0, n = cookieList.size(); i != n; ++i)
               {
                  if (i != 0)
                  {
                     buf.append("; ");
                  }

                  cookie = (Cookie)cookieList.get(i);

                  buf.append(cookie.getName());
                  buf.append('=');
                  buf.append(cookie.getValue());
               }

               headerMap.set(new MIMEHeader(HTTP.HEADER_COOKIE, buf.toString()));
            }
         }
      }
   }

   /**
    * Determines if the protocol is at least HTTP/1.1.
    * @param sProtocol The HTTP protocol.
    * @return True if the protocol is at least HTTP/1.1.
    */
   protected boolean isProtocolCurrent(String sProtocol)
   {
      return sProtocol != null &&
         sProtocol.regionMatches(0, HTTP.HTTP_1_1, 0, HTTP.HTTP_LENGTH) &&
         sProtocol.compareTo(HTTP.HTTP_1_1) >= 0;
   }

   /**
    * @return True if a request has only headers.
    */
   protected boolean isRequestEmpty()
   {
      return m_sMethod.equals(HTTP.METHOD_GET) ||
         m_sMethod.equals(HTTP.METHOD_HEAD) ||
         m_sMethod.equals(HTTP.METHOD_DELETE);
   }

   /**
    * @return True if the content length has to be computed.
    */
   protected boolean isLengthRequired()
   {
      if (m_sProtocol == null)
      {
         return false;
      }

      MIMEHeader header = m_requestHeaderMap.find(HTTP.HEADER_CONTENT_LENGTH);

      if (header != null)
      {
         String sLength = header.getValue();

         if (sLength == null || sLength.length() == 0 || sLength.charAt(0) == '-')
         {
            return true;
         }
      }

      if (!isProtocolCurrent(m_sProtocol))
      {
         return true;
      }

      if (m_bChunked)
      {
         return false;
      }

      header = m_requestHeaderMap.find(HTTP.HEADER_TRANSFER_ENCODING);

      return header == null || header.getValueCount() == 0 ||
         !header.getValue(header.getValueCount() - 1).getName().equalsIgnoreCase("chunked");
   }

   /**
    * Writes the request to the output stream.
    * @param ostream The output stream.
    * @param bTunnelRequest True to establish an HTTP tunnel through the proxy server; false otherwise;
    */
   protected void startRequest(HTTPOutputStream ostream, boolean bTunnelRequest) throws IOException
   {
      ostream.write((bTunnelRequest) ? HTTP.METHOD_CONNECT : m_sMethod);
      ostream.write(' ');

      if (bTunnelRequest)
      {
         ostream.write(m_sHostPort);

         if (m_sHostPort.indexOf(':') < 0)
         {
            ostream.write(":443");
         }
      }
      else
      {
         if (isHTTPProxy() && m_nTunnelStatus == TUNNEL_NONE)
         {
            ostream.write(m_uri.getScheme());
            ostream.write("://");
            ostream.write(m_sHostPort);
         }

         String sPath = m_uri.getRawPath();

         if (sPath == null || sPath.length() == 0)
         {
            sPath = "/";
         }

         ostream.write(sPath);

         String sQuery = m_uri.getRawQuery();

         if (sQuery != null)
         {
            ostream.write('?');
            ostream.write(sQuery);
         }

         String sFragment = m_uri.getRawFragment();

         if (sFragment != null)
         {
            ostream.write('#');
            ostream.write(sFragment);
         }
      }

      if (m_sProtocol != null)
      {
         ostream.write(' ');
         ostream.write(m_sProtocol);
         ostream.write("\r\n");

         for (int i = 0, n = m_requestHeaderMap.size(); i < n; ++i)
         {
            MIMEHeader header = m_requestHeaderMap.get(i);
            String sHeaderName = header.getName();

            if (bTunnelRequest && !s_proxyHeaderSet.contains(sHeaderName))
            {
               continue;
            }

            ostream.write(sHeaderName);
            ostream.write(": ");
            ostream.write(header.getValue());
            ostream.write("\r\n");
         }
      }

      ostream.write("\r\n");
   }

   /**
    * Finds a cached socket.
    * @param bSSL True if an SSL socket is needed.
    * @param address The socket address.
    * @param endpointAddress The address of the proxy SSL tunnel's end point (i.e. the server); null if no tunnel.
    */
   protected Socket findSocket(boolean bSSL, SocketAddress address, SocketAddress endpointAddress)
   {
      long lTime = m_lTime;
      List list = null;

      try
      {
         synchronized (s_connectionMap)
         {
            lTime -= s_lCacheTimeout;

            for (;;)
            {
               Connection con = (Connection)s_connectionDeque.first();

               if (con == null)
               {
                  return null;
               }

               if (con.getTime() > lTime)
               {
                  break;
               }

               s_connectionDeque.removeFirst();

               if (list == null)
               {
                  list = new ArrayList();
               }

               list.add(con.getSocket());

               if (con.remove())
               {
                  s_connectionMap.remove(con.getKey());
               }
               else if (s_connectionMap.get(con.getKey()) == con)
               {
                  s_connectionMap.put(con.getKey(), con.getNext());
               }
            }

            if (m_key == null)
            {
               m_key = new ConnectionKey();
            }

            m_key.setKey(bSSL, address, m_trustedCertificate, m_clientCertificateStore, endpointAddress,
               (isHTTPProxy()) ? m_proxyAuthentication : null);

            Connection con = (Connection)s_connectionMap.get(m_key);

            if (con == null)
            {
               return null;
            }

            s_connectionDeque.remove(con);

            if (con.remove())
            {
               s_connectionMap.remove(con.getKey());
            }
            else
            {
               s_connectionMap.put(con.getKey(), con.getNext());
            }

            return con.getSocket();
         }
      }
      finally
      {
         closeSockets(list);
      }
   }

   /**
    * Caches a socket.
    * @param bSSL The SSL flag.
    * @param address The socket address.
    * @param socket The socket.
    * @param endpointAddress The address of the proxy SSL tunnel's end point (i.e. the server); null if no tunnel.
    * @return True if the socket has been cached.
    */
   protected boolean cacheSocket(boolean bSSL, SocketAddress address, Socket socket, SocketAddress endpointAddress)
   {
      if (s_logger.isDebugEnabled() && !m_bStealth)
      {
         s_logger.debug("Caching socket " + socket);
      }

      if (m_key == null)
      {
         m_key = new ConnectionKey();
      }

      m_key.setKey(bSSL, address, m_trustedCertificate, m_clientCertificateStore, endpointAddress,
         (isHTTPProxy()) ? m_proxyAuthentication : null);

      Connection con = new Connection(m_key, socket, System.currentTimeMillis());
      List list = null;

      m_key = null;

      try
      {
         synchronized (s_connectionMap)
         {
            Connection next = (Connection)s_connectionMap.put(con.getKey(), con);

            if (next != null)
            {
               next.add(con);
            }

            s_connectionDeque.addLast(con);

            while (s_connectionDeque.size() > s_nCacheLimit)
            {
               con = (Connection)s_connectionDeque.removeFirst();

               if (con == null)
               {
                  break;
               }

               if (list == null)
               {
                  list = new ArrayList();
               }

               list.add(con.getSocket());

               if (con.remove())
               {
                  s_connectionMap.remove(con.getKey());
               }
               else if (s_connectionMap.get(con.getKey()) == con)
               {
                  s_connectionMap.put(con.getKey(), con.getNext());
               }
            }
         }

         return true;
      }
      finally
      {
         closeSockets(list);
      }
   }

   /**
    * Checks the connection cache for expired sockets and closes them.
    */
   public static void maintainCache()
   {
      long lTime = System.currentTimeMillis();
      List list = null;

      try
      {
         synchronized (s_connectionMap)
         {
            lTime -= s_lCacheTimeout;

            for (Iterator itr = s_connectionDeque.iterator(); itr.hasNext();)
            {
               Connection con = (Connection)itr.next();

               if (con.getTime() <= lTime)
               {
                  itr.remove();

                  if (list == null)
                  {
                     list = new ArrayList();
                  }

                  list.add(con.getSocket());

                  if (con.remove())
                  {
                     s_connectionMap.remove(con.getKey());
                  }
                  else if (s_connectionMap.get(con.getKey()) == con)
                  {
                     s_connectionMap.put(con.getKey(), con.getNext());
                  }
               }
            }
         }
      }
      finally
      {
         closeSockets(list);
      }
   }

   /**
    * Closes all the sockets in the list.
    * @param socketList The list of sockets to close. Can be null.
    */
   protected static void closeSockets(List socketList)
   {
      if (socketList != null)
      {
         for (int i = 0, n = socketList.size(); i < n; ++i)
         {
            Socket socket = (Socket)socketList.get(i);

            try
            {
               if (s_logger.isDebugEnabled())
               {
                  s_logger.debug("Closing socket " + socket);
               }

               socket.close();
            }
            catch (IOException e)
            {
               s_logger.debug("Socket closing error", e);
            }
         }
      }
   }

   /**
    * Connects to the server.
    */
   protected void connect() throws IOException
   {
      m_lTime = System.currentTimeMillis();

      if (m_socket != null)
      {
         if (isHTTPProxy() && m_nTunnelStatus == TUNNEL_READY)
         {
            if (s_logger.isDebugEnabled())
            {
               s_logger.debug("Layering SSL socket to " + m_sHost + ":" + m_uri.getPort() + " over tunnel socket: " + m_socket);
            }

            m_socket = getSocketFactory().createSocket(m_socket, m_sHost, m_uri.getPort(), true);
            m_nTunnelStatus = TUNNEL_ESTABLISHED;
         }

         return;
      }

      m_bCachedSocket = false;

      int nPort = m_uri.getPort();
      boolean bSSL = false;

      if (!m_bPresetProxy)
      {
         m_proxy = (Proxy)ProxySelector.getDefault().select(m_uri).get(0);
      }

      m_sHostPort = m_sHost = (m_uri.getHost() != null) ? m_uri.getHost() : InetAddress.getByName(null).getHostName();

      if (nPort >= 0)
      {
         m_sHostPort += ":" + nPort;
      }

      if (HTTP.SCHEME_HTTP.equalsIgnoreCase(m_uri.getScheme()))
      {
         if (nPort < 0)
         {
            nPort = 80;
         }
      }
      else if (HTTP.SCHEME_SSL.equalsIgnoreCase(m_uri.getScheme()))
      {
         if (nPort < 0)
         {
            nPort = 443;
         }

         bSSL = true;
      }
      else
      {
         throw new HTTPClientException("Unknown scheme \"" + m_uri.getScheme() + "\"");
      }

      SocketAddress address;
      boolean bNew = false;
      final int nSocketPort = nPort;

      m_serverAddress = (SocketAddress)AccessController.doPrivileged(
         new PrivilegedAction()
         {
            public Object run()
            {
               return new InetSocketAddress(m_sHost, nSocketPort);
            }
         });

      if (m_proxy != null && m_proxy.type() == Proxy.Type.HTTP)
      {
         address = resolve((InetSocketAddress)m_proxy.address());

         if ((m_socket = findSocket(bSSL, address, (bSSL) ? m_serverAddress : null)) == null)
         {
            m_socket = new Socket();
            bNew = true;
         }
      }
      else
      {
         address = m_serverAddress;

         if ((m_socket = findSocket(bSSL, address, null)) == null)
         {
            m_socket = (Socket)AccessController.doPrivileged(
               new PrivilegedAction()
               {
                  public Object run()
                  {
                     return new Socket(m_proxy);
                  }
               });

            bNew = true;
         }
      }

      if (s_logger.isDebugEnabled() && !m_bStealth)
      {
         s_logger.debug("Connecting to " + address + ((bNew) ? " (new)" : " (persistent)"));
      }

      if (bNew)
      {
         m_socket.bind((SocketAddress)AccessController.doPrivileged(
            new PrivilegedAction()
            {
               public Object run()
               {
                  return new InetSocketAddress(0);
               }
            }));

         m_socket.setKeepAlive(true);
         m_socket.setSoTimeout(m_nReadTimeout);
         m_socket.connect(address, m_nConnectionTimeout);

         if (bSSL)
         {
            if (m_nTunnelStatus == TUNNEL_NONE)
            {
               m_socket = getSocketFactory().createSocket(m_socket, m_sHost, nPort, true);
            }
         }
      }
      else
      {
         m_nTunnelStatus = (m_nTunnelStatus == TUNNEL_CREATE) ? TUNNEL_ESTABLISHED : TUNNEL_NONE;
         m_bCachedSocket = true;
      }
   }

   /**
    * Resolves the address if it is unresolved.
    *
    * @param address The address to resolve.
    * @return The resolved address.
    */
   protected static InetSocketAddress resolve(final InetSocketAddress address)
   {
      if (address.isUnresolved())
      {
         return (InetSocketAddress)AccessController.doPrivileged(
            new PrivilegedAction()
            {
               public Object run()
               {
                  return new InetSocketAddress(((InetSocketAddress)address).getHostName(), ((InetSocketAddress)address).getPort());
               }
            }
         );
      }
      else
      {
         return address;
      }
   }

   /**
    * Gets the connection factory for SSL sockets. Uses keystores if set on the client, otherwise uses the
    * default Java keystore.
    * @return The connection factory for SSL sockets.
    */
   protected SSLSocketFactory getSocketFactory()
   {
      if (m_clientCertificateStore == null && m_trustedCertificate == null)
      {
         synchronized (HTTPClient.class)
         {
            if (s_sslSocketFactory == null)
            {
               AccessController.doPrivileged(
                  new PrivilegedAction()
                  {
                     public Object run()
                     {
                        try
                        {
                           s_sslSocketFactory = ((HttpsURLConnection)new URL("https:///").openConnection()).getSSLSocketFactory();
                        }
                        catch (Exception e)
                        {
                           s_logger.debug("Unable to obtain the HTTPS URL connection socket factory; using the default", e);
                           s_sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
                        }

                        return null;
                     }
                  });
            }

            return s_sslSocketFactory;
         }
      }
      else
      {
         return (SSLSocketFactory)AccessController.doPrivileged(
            new PrivilegedAction()
            {
               public Object run()
               {
                  try
                  {
                     SSLContext context = SSLContext.getInstance("TLS");

                     context.init(
                        CertificateUtil.getKeyManagers(m_clientCertificateStore, m_achClientCertificatePassword),
                        CertificateUtil.getTrustManagers(m_trustedCertificate),
                        RandUtil.getSecureRandom());

                     return context.getSocketFactory();
                  }
                  catch (GeneralSecurityException ex)
                  {
                     throw ObjUtil.rethrow(ex);
                  }
               }
            });
      }
   }

   /**
    * Disconnects the client.
    * @param bCache True to cache the socket.
    */
   protected void disconnect(boolean bCache) throws IOException
   {
      if (m_socket != null)
      {
         if (!bCache || !cacheSocket(m_socket instanceof SSLSocket, m_socket.getRemoteSocketAddress(), m_socket,
            (m_nTunnelStatus == TUNNEL_ESTABLISHED) ? m_serverAddress : null))
         {
            if (s_logger.isDebugEnabled() && !m_bStealth)
            {
               s_logger.debug("Closing socket " + m_socket);
            }

            try
            {
               m_socket.close();
            }
            catch (IOException e)
            {
               s_logger.debug("Socket closing error", e);
            }
         }

         m_socket = null;
         m_serverAddress = null;
         m_bCachedSocket = false;
      }
   }

   /**
    * Completes the request/response cycle.
    * @param istream The HTTP input stream.
    */
   protected void complete(HTTPInputStream istream) throws IOException
   {
      istream.close();

      // Close the socket, if necessary

      if (m_sProtocol == null || !istream.isChunked() && istream.getMaxCount() < 0)
      {
         disconnect(false);
      }
      else
      {
         MIMEHeader header = m_responseHeaderMap.find(HTTP.HEADER_CONNECTION);

         if (header == null)
         {
            if (!isProtocolCurrent(m_sResponseProtocol))
            {
               disconnect(false);
            }
         }
         else if (m_sProtocol.equals(HTTP.HTTP_1_0) && header.findValue("keep-alive") == null ||
            header.findValue("close") != null)
         {
            disconnect(false);
         }
      }
   }

   /**
    * Skips the response, if needed.
    * @return True if the response must be skipped.
    */
   protected boolean skip() throws IOException
   {
      return m_nResponseStatus / 100 == 1;
   }

   /**
    * Redirects the client.
    * @return True if redirection is required.
    */
   protected boolean redirect() throws IOException
   {
      if (m_nResponseStatus == HTTP.STATUS_REQUEST_TIMEOUT)
      {
         return true;
      }

      return false;
   }

   /**
    * Sets the request authentication token.
    * @param nProtocol The authentication protocol, one of the AUTH_* constants.
    * @param token The authentication token.
    */
   protected void setAuthToken(int nProtocol, byte[] token) throws IOException
   {
      if (m_currentAuthentication.isProxy())
      {
         m_proxyAuthHeader = m_requestHeaderMap.set(HTTP.PROXY_AUTH_REQUEST_HEADER,
            ((nProtocol == AUTH_BASIC) ? HTTP.BASIC_PROTOCOL : HTTP.SPNEGO_PROTOCOL) + " " + Base64Util.encode(token));

         if (nProtocol != AUTH_BASIC)
         {
            m_proxyAuthHeader = null;
         }

         // Maintain server authentication (for Squid)
         if (m_authHeader != null)
         {
            m_requestHeaderMap.set(m_authHeader);
         }
      }
      else
      {
         m_authHeader = m_requestHeaderMap.set(HTTP.AUTH_REQUEST_HEADER,
            ((nProtocol == AUTH_BASIC) ? HTTP.BASIC_PROTOCOL : HTTP.SPNEGO_PROTOCOL) + " " + Base64Util.encode(token));

         if (nProtocol != AUTH_BASIC)
         {
            m_authHeader = null;
         }

         // Maintain proxy authentication (for Squid)
         if (m_proxyAuthHeader != null)
         {
            m_requestHeaderMap.set(m_proxyAuthHeader);
         }
      }
   }

   /**
    * @return True if the proxy is an HTTP proxy; false otherwise (or if no proxy set).
    */
   protected boolean isHTTPProxy()
   {
      return m_proxy != null && m_proxy.type() == Proxy.Type.HTTP;
   }

   /**
    * Retrieves the user name and the password.
    * @return True if they are different object from the previous ones.
    */
   protected boolean retrievePassword()
   {
      if (m_currentAuthentication.getUser() == null)
      {
         PasswordAuthenticationProvider provider = m_currentAuthentication.getProvider();

         if (provider == null)
         {
            return false;
         }

         if (provider instanceof Prompt)
         {
            ((Prompt)provider).setPrompt((m_currentAuthentication.getUserSaved() == null) ? null : "Invalid password");
         }

         PasswordAuthentication pwa = provider.getPasswordAuthentication();

         m_currentAuthentication.setUser((pwa == null) ? null : pwa.getUserName());
         m_currentAuthentication.setPassword((pwa == null) ? null : pwa.getPassword());

         if (m_currentAuthentication.isCurrentMatchedBySaved())
         {
            return false;
         }

         m_currentAuthentication.copyToSaved();
      }

      return true;
   }

   /**
    * Resets the authentication state.
    */
   protected void resetAuth()
   {
      m_sToken = null;
      m_bAuthDone = false;
      m_requestHeaderMap.remove(HTTP.AUTH_REQUEST_HEADER);
      m_requestHeaderMap.remove(HTTP.PROXY_AUTH_REQUEST_HEADER);
   }

   /**
    * Authenticates the client, if necessary.
    * @return True if the handshake is complete or not needed.
    */
   protected boolean authenticate() throws IOException
   {
      switch (m_nResponseStatus)
      {
         case HTTP.STATUS_UNAUTHORIZED:

            if (m_currentAuthentication.isProxy())
            {
               completeAuthentication();
               m_bAuthDone = false;
               m_nBasicCount = -1;
            }

            m_currentAuthentication = m_serverAuthentication;

            break;

         case HTTP.STATUS_PROXY_AUTHENTICATION:

            // Disallow proxy auth if no proxy or through a tunnel
            if (!isHTTPProxy() || m_nTunnelStatus == TUNNEL_ESTABLISHED)
            {
               throw new LoginException("Proxy authentication disallowed to non-proxy");
            }

            m_currentAuthentication = m_proxyAuthentication;

            break;

         case HTTP.STATUS_FORBIDDEN:
            if (s_logger.isDebugEnabled())
            {
               s_logger.debug("Authentication error " +
                  m_nResponseStatus + ": " + m_sResponseMessage);
            }

            m_bAuthDone = false;

            if (m_currentAuthentication.getProvider() == null || m_currentAuthentication.getProvider().isAuthenticationDeterministic())
            {
               return true;
            }

            if (m_currentAuthentication.getSPNEGO() == SPNEGO_SILENT)
            {
               m_currentAuthentication.setSPNEGO(SPNEGO_CRED);
               m_nBasicCount = 0;
               m_sToken = null;

               if (m_authenticator != null)
               {
                  m_authenticator.dispose();
                  m_authenticator = null;
               }

               resetAuth();

               return false;
            }

            break;

         default:
            completeAuthentication();

            return true;
      }

      if (m_nBasicCount < 0 && m_currentAuthentication.getUserSaved() == null)
      {
         m_nBasicCount = 0;
      }

      if (m_bAuthDone)
      {
         if (s_logger.isDebugEnabled())
         {
            s_logger.debug("Authentication error " +
               m_nResponseStatus + ": " + m_sResponseMessage);
         }
      }

      int nProtocol = m_currentAuthentication.parseAuthProtocol();

      while (nProtocol == AUTH_SPNEGO)
      {
         if (m_authenticator == null)
         {
            try
            {
               m_authenticator = AuthenticatorFactory.create();
            }
            catch (Exception e)
            {
               m_serverAuthentication.setSPNEGO(SPNEGO_NONE);
               m_proxyAuthentication.setSPNEGO(SPNEGO_NONE);
               nProtocol = m_currentAuthentication.parseAuthProtocol();
            }
         }

         if (m_authenticator != null)
         {
            try
            {
               byte[] token;

               if (m_sToken == null)
               {
                  if (m_currentAuthentication.getSPNEGO() == SPNEGO_SILENT && !m_bAuthDone)
                  {
                     // Use mutual authentication due to bug #6733095 in Sun's JRE: http://bugs.sun.com/view_bug.do?bug_id=6733095
                     m_authenticator.init(Authenticator.PROTOCOL_SPNEGO, Authenticator.MODE_MUTUAL, m_currentAuthentication.getServiceName(), null, null, null);
                  }
                  else
                  {
                     m_currentAuthentication.setSPNEGO(SPNEGO_CRED);
                     m_bAuthDone = false;

                     if (m_nBasicCount < 0)
                     {
                        m_currentAuthentication.copyFromSaved();
                     }
                     else
                     {
                        m_currentAuthentication.clearUserPassword();
                     }

                     if (m_nBasicCount++ == MAX_BASIC_ATTEMPTS || !retrievePassword())
                     {
                        throw new IOException("Too many authentication attempts");
                     }

                     // Use mutual authentication due to bug #6733095 in Sun's JRE: http://bugs.sun.com/view_bug.do?bug_id=6733095
                     m_authenticator.init(Authenticator.PROTOCOL_SPNEGO, Authenticator.MODE_MUTUAL, m_currentAuthentication.getServiceName(), null, m_currentAuthentication.getUser(), m_currentAuthentication.getPassword());
                  }

                  if (s_logger.isDebugEnabled())
                  {
                     s_logger.debug("Starting authentication sequence with service \"" + m_currentAuthentication.getServiceName() +
                        ((m_currentAuthentication.getUser() == null) ? "\"" : "\", user \"" + m_currentAuthentication.getUser() + "\""));
                  }

                  token = m_authenticator.nextToken(null);
               }
               else
               {
                  token = Base64Util.decode(m_sToken);

                  if (s_logger.isDebugEnabled())
                  {
                     s_logger.debug("Authentication response token " + new Binary(token));
                  }

                  token = m_authenticator.nextToken(token);
               }

               resetAuth();

               if (token != null)
               {
                  if (s_logger.isDebugEnabled())
                  {
                     s_logger.debug("Authentication request token " + new Binary(token));
                  }

                  setAuthToken(AUTH_SPNEGO, token);
               }

               if (m_authenticator.isDone())
               {
                  m_authenticator.dispose();
                  m_authenticator = null;
                  m_bAuthDone = true;
               }

               return false;
            }
            catch (CancellationException e)
            {
               throw e;
            }
            catch (Exception e)
            {
               s_logger.debug("Authentication error", e);

               if (m_currentAuthentication.getSPNEGO() == SPNEGO_SILENT &&
                  e instanceof LoginException)
               {
                  m_currentAuthentication.setSPNEGO(SPNEGO_CRED);
               }
               else
               {
                  m_currentAuthentication.setSPNEGO(SPNEGO_NONE);
                  nProtocol = m_currentAuthentication.parseAuthProtocol();
                  m_authenticator.dispose();
                  m_authenticator = null;
               }

               m_nBasicCount = 0;
               m_sToken = null;
            }
         }
      }

      if (nProtocol == AUTH_BASIC)
      {
         if (m_nBasicCount < 0)
         {
            m_currentAuthentication.copyFromSaved();
         }

         if (m_nBasicCount++ == MAX_BASIC_ATTEMPTS || !retrievePassword())
         {
            m_authHeader = null;

            throw new LoginException();
         }

         StringBuffer buf = new StringBuffer(32);

         if (s_logger.isDebugEnabled())
         {
            s_logger.debug("Attempting basic authentication for user \"" + m_currentAuthentication.getUser() + "\"");
         }

         if (m_currentAuthentication.getUser() != null)
         {
            buf.append(m_currentAuthentication.getUser());
         }

         buf.append(':');

         if (m_currentAuthentication.getPassword() != null)
         {
            buf.append(m_currentAuthentication.getPassword());
         }

         byte[] token = buf.toString().getBytes("UTF-8");

         for (int i = 0; i < buf.length(); ++i)
         {
            buf.setCharAt(i, ' ');
         }

         m_currentAuthentication.setUser(null);

         resetAuth();
         setAuthToken(AUTH_BASIC, token);
         Arrays.fill(token, (byte)0);

         return false;
      }

      throw new LoginException();
   }

   /**
    * Completes authentication. If using an authenticator then it is disposed.
    *
    * @throws IOException If an I/O error occurs.
    */
   protected void completeAuthentication() throws IOException
   {
      if (m_authenticator != null)
      {
         if (m_currentAuthentication.parseAuthProtocol() == AUTH_SPNEGO && m_sToken != null)
         {
            try
            {
               byte[] token = Base64Util.decode(m_sToken);

               if (s_logger.isDebugEnabled())
               {
                  s_logger.debug("Authentication response token " + new Binary(token));
               }

               if (!m_bSPNEGOStrict ||
                  m_authenticator.nextToken(token) == null &&
                  m_authenticator.isDone())
               {
                  m_authenticator.dispose();
                  m_authenticator = null;

                  s_logger.debug("Authentication complete");

                  return;
               }
            }
            catch (Exception e)
            {
               s_logger.debug("Authentication error", e);
            }
         }
         else if (m_bSPNEGOStrict)
         {
            throw new AuthenticationException("err.auth.server");
         }

         m_authenticator.dispose();
         m_authenticator = null;
      }
      else if (m_bAuthDone)
      {
         s_logger.debug("Authentication complete");
      }
   }

   // inner classes

   /**
    * HTTP request handler.
    */
   public interface RequestHandler
   {
      /**
       * Outputs the request.
       * @param client The HTTP client.
       * @param ostream The output stream. Null, if the method does not support a request body.
       * @throws IOException if an I/O error occurs.
       */
      void handleRequest(HTTPClient client, OutputStream ostream) throws IOException;
   }

   /**
    * HTTP response handler.
    */
   public interface ResponseHandler
   {
      /**
       * Inputs the response.
       * @param client The HTTP client.
       * @param istream The input stream. Null, if the method or the error code does not support a response body.
       * @return The HTTPClient.invoke() result.
       * @throws IOException if an I/O error occurs.
       */
      Object handleResponse(HTTPClient client, InputStream istream) throws IOException;
   }

   /**
    * Output stream for generating an HTTP request.
    */
   protected class HTTPOutputStream extends MIMEOutputStream
   {
      // constants

      /**
       * The buffer offset for chunked encoding.
       */
      protected final static int BUF_OFS = 8;

      // attributes

      /**
       * The buffer byte count.
       */
      protected int m_nCount;

      /**
       * The chunked segment offset in the buffer.
       */
      protected int m_nOffset;

      /**
       * True if the stream has been closed.
       */
      protected boolean m_bClosed;

      /**
       * The chunked transfer encoding flag.
       */
      protected boolean m_bChunked;

      // associations

      /**
       * The stream butter.
       */
      protected byte[] m_buf = new byte[BUF_OFS + BUF_SIZE + BUF_OFS + 8];

      // constructors

      /**
       * Constructs an HTTP output stream.
       * @param ostream The socket output stream.
       */
      public HTTPOutputStream(OutputStream ostream)
      {
         super(ostream);
         m_nCount = m_nOffset = BUF_OFS;
      }

      // operations

      /**
       * Sets the chunked transfer encoding flag.
       * @param bChunked The chunked transfer encodign flag to set.
       */
      public void setChunked(boolean bChunked)
      {
         m_bChunked = bChunked;
         m_nOffset = m_nCount;
      }

      /**
       * @return The chunked transfer encoding flag.
       */
      public boolean isChunked()
      {
         return m_bChunked;
      }

      /**
       * @see nexj.core.util.MIMEOutputStream#write(int)
       */
      public void write(int ch) throws IOException
      {
         if (m_nCount == BUF_OFS + BUF_SIZE)
         {
            flush();
         }

         m_buf[m_nCount++] = (byte)ch;
      }

      /**
       * @see nexj.core.util.MIMEOutputStream#flush()
       */
      public void flush() throws IOException
      {
         if (m_nCount != BUF_OFS)
         {
            flush(m_nCount);
         }
      }

      /**
       * Flushes the buffer.
       * @param nCount The chunk byte count.
       */
      protected void flush(int nCount) throws IOException
      {
         int nOfs = BUF_OFS;

         if (m_bChunked && nCount != m_nOffset)
         {
            m_buf[--nOfs] = '\n';
            m_buf[--nOfs] = '\r';
            nCount -= m_nOffset;

            do
            {
               int nDigit = nCount & 0x0f;

               m_buf[--nOfs] = (byte)(nDigit + ((nDigit < 10) ? '0' : 'A' - 10));
               nCount >>>= 4;
            }
            while (nCount != 0);

            if (m_nOffset != BUF_OFS)
            {
               System.arraycopy(m_buf, nOfs, m_buf, m_buf.length - BUF_OFS, BUF_OFS - nOfs);
               System.arraycopy(m_buf, BUF_OFS, m_buf, nOfs, m_nOffset - BUF_OFS);
               System.arraycopy(m_buf, m_buf.length - BUF_OFS, m_buf, m_nOffset - BUF_OFS + nOfs, BUF_OFS - nOfs);
            }

            m_buf[m_nCount++] = '\r';
            m_buf[m_nCount++] = '\n';
         }

         m_ostream.write(m_buf, nOfs, m_nCount - nOfs);
         m_nCount = m_nOffset = BUF_OFS;
         m_ostream.flush();
      }

      /**
       * @see java.io.OutputStream#close()
       */
      public void close() throws IOException
      {
         if (!m_bClosed)
         {
            int nCount = m_nCount;

            if (m_bChunked && nCount != m_nOffset)
            {
               m_buf[m_nCount++] = '\r';
               m_buf[m_nCount++] = '\n';
               m_buf[m_nCount++] = '0';
               m_buf[m_nCount++] = '\r';
               m_buf[m_nCount++] = '\n';
            }

            if (m_bChunked || nCount != BUF_OFS)
            {
               flush(nCount);
            }

            m_bClosed = true;


         }
      }
   }

   /**
    * Input stream for parsing an HTTP response.
    */
   protected class HTTPInputStream extends MIMEInputStream
   {
      // attributes

      /**
       * Current stream byte count.
       */
      protected long m_lCount;

      /**
       * Maximum stream byte count.
       */
      protected long m_lMaxCount = -1;

      /**
       * The chunked transfer encoding flag.
       */
      protected boolean m_bChunked;

      /**
       * End-of-File flag.
       */
      protected boolean m_bEOF;

      // constructors

      /**
       * Constructs an HTTP input stream.
       * @param istream The socket input stream.
       */
      protected HTTPInputStream(InputStream istream)
      {
         super(istream, ENCODING);
      }

      // operations

      /**
       * @return The maximum stream byte count.
       */
      public long getMaxCount()
      {
         return m_lMaxCount;
      }

      /**
       * @return The chunked transfer encoding flag.
       */
      public boolean isChunked()
      {
         return m_bChunked;
      }

      /**
       * @see nexj.core.util.MIMEInputStream#read()
       */
      public int read() throws IOException
      {
         for (;;)
         {
            if (m_lCount < m_lMaxCount)
            {
               int ch = m_istream.read();

               if (ch < 0)
               {
                  throw new HTTPClientException("Premature end of stream");
               }

               ++m_lCount;

               return ch;
            }

            if (m_lMaxCount < 0)
            {
               return m_istream.read();
            }

            if (m_bChunked && !m_bEOF)
            {
               long lCount = 0;
               boolean bSize = false;
               int ch;

               if (m_lMaxCount != 0)
               {
                  ch = m_istream.read();

                  if (ch == CHAR_CR)
                  {
                     ch = m_istream.read();
                  }

                  if (ch != CHAR_LF)
                  {
                     throw new HTTPClientException("Invalid chunk delimiter");
                  }
               }

               for (;;)
               {
                  ch = m_istream.read();

                  if (ch < 0)
                  {
                     break;
                  }

                  int nDigit = Character.digit((char)ch, 16);

                  if (nDigit < 0 || (lCount & ~(~0L >>> 4)) != 0)
                  {
                     break;
                  }

                  lCount = (lCount << 4) | nDigit;
                  bSize = true;
               }

               if (!bSize || ch < 0 || !Character.isWhitespace((char)ch) && ch != ';')
               {
                  throw new HTTPClientException("Invalid chunk size");
               }

               while (ch != CHAR_LF)
               {
                  if (ch < 0)
                  {
                     throw new HTTPClientException("Invalid chunk");
                  }

                  ch = m_istream.read();
               }

               m_lCount = 0;

               if (lCount != 0)
               {
                  m_lMaxCount = lCount;

                  continue;
               }

               m_lMaxCount = -1;
               parseHeaders(m_responseHeaderMap);
               m_lMaxCount = 0;
               m_bEOF = true;
            }

            return -1;
         }
      }

      /**
       * @see java.io.InputStream#read(byte[], int, int)
       */
      public int read(byte[] buf, int nOfs, int nCount) throws IOException
      {
         if (nCount <= 0 || m_bEOF)
         {
            return -1;
         }

         if (m_lCount < m_lMaxCount)
         {
            int n = m_istream.read(buf, nOfs, (int)Math.min(nCount, m_lMaxCount - m_lCount));

            if (n <= 0)
            {
               throw new HTTPClientException("Premature end of stream");
            }

            m_lCount += n;

            return n;
         }

         if (m_lMaxCount < 0)
         {
            return m_istream.read(buf, nOfs, nCount);
         }

         if (m_bChunked)
         {
            int ch = read();

            if (ch >= 0)
            {
               buf[nOfs] = (byte)ch;

               return 1;
            }
         }

         return -1;
      }

      /**
       * Starts processing the response.
       * @return True if the response header has been processed, false if reconnection is required.
       */
      public boolean start() throws IOException
      {
         m_lCount = 0;
         m_lMaxCount = -1;
         m_responseHeaderMap.clear();

         if (!parseStatusLine())
         {
            return false;
         }

         parseHeaders(m_responseHeaderMap);

         if (m_nResponseStatus / 100 == 1 || m_nResponseStatus == HTTP.STATUS_NO_CONTENT ||
            m_nResponseStatus == HTTP.STATUS_NOT_MODIFIED || m_sMethod.equals(HTTP.METHOD_HEAD))
         {
            m_lMaxCount = 0;
         }
         else
         {
            MIMEHeader header = m_responseHeaderMap.find(HTTP.HEADER_TRANSFER_ENCODING);

            if (header != null && header.findValue("chunked") != null && header.getValueCount() == 1)
            {
               m_responseHeaderMap.remove(HTTP.HEADER_CONTENT_LENGTH);
               m_lMaxCount = 0;
               m_bChunked = true;
            }
            else
            {
               header = m_responseHeaderMap.find(HTTP.HEADER_CONTENT_LENGTH);

               if (header != null)
               {
                  try
                  {
                     m_lMaxCount = Long.parseLong(header.getValue());

                     if (m_lMaxCount < 0)
                     {
                        m_lMaxCount = 0;
                     }
                  }
                  catch (NumberFormatException e)
                  {
                     throw new HTTPClientException("Invalid content length", e);
                  }
               }
            }
         }

         m_lCount = 0;

         return true;
      }

      /**
       * Parses the HTTP status line.
       * @return True if the status line has been parsed, false to reconnect.
       */
      protected boolean parseStatusLine() throws IOException
      {
         StringBuffer buf = new StringBuffer(32);

         m_sResponseProtocol = null;
         m_sResponseMessage = "";
         m_nResponseStatus = 0;

         // Check if the response starts with "HTTP/"
         m_istream.mark(HTTP.HTTP_LENGTH);

         for (int i = 0; i < HTTP.HTTP_LENGTH; ++i)
         {
            int ch;

            if (i == 0)
            {
               try
               {
                  ch = m_istream.read();
               }
               catch (InterruptedIOException e)
               {
                  throw e;
               }
               catch (IOException e)
               {
                  return false;
               }
            }
            else
            {
               ch = m_istream.read();
            }

            if (ch < 0 || ch != HTTP.HTTP_1_1.charAt(i))
            {
               m_istream.reset();

               if (i == 0)
               {
                  return false;
               }

               buf.setLength(0);

               break;
            }

            buf.append((char)ch);
         }

         m_istream.mark(0);

         if (buf.length() == 0)
         {
            m_nResponseStatus = 200;
         }
         else
         {
            for (int ch; (ch = m_istream.read()) >= 0;)
            {
               if (ch == CHAR_LF)
               {
                  int n = buf.length();

                  if (n != 0 && buf.charAt(n - 1) == CHAR_CR)
                  {
                     buf.setLength(n - 1);
                  }

                  break;
               }

               buf.append((char)ch);
            }

            int i = HTTP.HTTP_LENGTH;
            int n = buf.length();

            while (i < n && buf.charAt(i) != ' ')
            {
               ++i;
            }

            m_sResponseProtocol = buf.substring(0, i);
            ++i;

            int k = i;

            while (i < n && buf.charAt(i) != ' ')
            {
               ++i;
            }

            if (k < i)
            {
               try
               {
                  m_nResponseStatus = Integer.parseInt(buf.substring(k, i));
               }
               catch (NumberFormatException e)
               {
                  throw new HTTPClientException("Invalid HTTP status code", e);
               }
            }
            else
            {
               throw new HTTPClientException("Missing HTTP status code");
            }

            ++i;

            if (i < n)
            {
               m_sResponseMessage = buf.substring(i);
            }
         }

         return true;
      }

      /**
       * @see nexj.core.util.MIMEInputStream#close()
       */
      public void close() throws IOException
      {
         while (read() >= 0) ;

         if (m_lMaxCount >= 0 && m_lCount != m_lMaxCount)
         {
            throw new HTTPClientException("Server has disconnected prematurely");
         }
      }
   }

   /**
    * Persistent connection.
    */
   protected static class Connection
   {
      // attributes

      /**
       * The timestamp when the socket was cached.
       */
      private long m_lTime;

      // associations

      /**
       * The connection key.
       */
      private ConnectionKey m_key;

      /**
       * The cached socket.
       */
      private Socket m_socket;

      /**
       * The previous connection in a circular list.
       */
      private Connection m_prev;

      /**
       * The next connection in a circular list.
       */
      private Connection m_next;

      // constructors

      /**
       * Constructs the persistent connection.
       * @param key The connection key.
       * @param socket The socket to cache.
       * @param lTime The current timestamp.
       */
      public Connection(ConnectionKey key, Socket socket, long lTime)
      {
         m_key = key;
         m_socket = socket;
         m_lTime = lTime;
         m_prev = m_next = this;
      }

      // operations

      /**
       * @return The connection key.
       */
      public ConnectionKey getKey()
      {
         return m_key;
      }

      /**
       * @return The cached socket.
       */
      public Socket getSocket()
      {
         return m_socket;
      }

      /**
       * @return The timestamp when the socket was cached.
       */
      public long getTime()
      {
         return m_lTime;
      }

      /**
       * @return The next connection.
       */
      public Connection getNext()
      {
         return m_next;
      }

      /**
       * Adds a connection to a circular list.
       * @param con The connection to add.
       */
      public void add(Connection con)
      {
         con.m_key = m_key;
         con.m_next = this;
         con.m_prev = m_prev;
         m_prev.m_next = con;
         m_prev = con;
      }

      /**
       * Removes itself from the circular list.
       * @return True if the list is empty after the operation.
       */
      public boolean remove()
      {
         m_next.m_prev = m_prev;
         m_prev.m_next = m_next;

         return m_next == this;
      }

      /**
       * @see java.lang.Object#toString()
       */
      public String toString()
      {
         StringBuilder buf = new StringBuilder(64);

         buf.append(m_socket);
         buf.append('(');
         buf.append(m_lTime);
         buf.append(')');

         return buf.toString();
      }
   };

   /**
    * Persistent connection key.
    *
    * The connection pool is divided along these attributes:
    * 1) SSL / not SSL: These are different types of connections; one may not be exchanged for the other.
    * 2) address (next-hop): The address of the network resource being connected. One cannot exchange a
    *    connection on one address for a connection on another.
    * 3) trusted cert and client cert: To prevent an unauthenticated SSL connection from using the credentials
    *    of an already-authenticated SSL connection.
    * 4) proxy credentials: SPNEGO authentication to a proxy is sticky to the connection through the proxy; only
    *    clients using the same proxy credentials should be allowed to share pooled connections.
    * 5) endpointAddress: An SSL tunnel through a proxy, once established, may only be used for additional
    *    communications with that endpoint.
    */
   protected static class ConnectionKey
   {
      // attributes

      /**
       * The cached hash code.
       */
      private int m_nHashCode;

      /**
       * The SSL flag.
       */
      private boolean m_bSSL;

      // associations

      /**
       * The socket address.
       */
      private SocketAddress m_address;

      /**
       * The address of the end point of the socket, if connection is a tunnel. For SSL tunnel, this
       * address is the address of the remote web server, and m_address is the proxy server address.
       */
      private SocketAddress m_endpointAddress;

      /**
       * The trust manager array when using client certificate authentication.
       */
      private Certificate m_trustedCertificate;

      /**
       * They key manager array when using client certificate authentication.
       */
      private KeyStore m_clientCertificateStore;

      /**
       * The proxy user.
       */
      private Object m_proxyUser;

      /**
       * The proxy password.
       */
      private char[] m_achProxyPassword;

      // operations

      /**
       * Sets the key.
       * @param bSSL The SSL flag.
       * @param address The socket address.
       * @param trustedCertificate The trusted certificate; null if not using client cert. auth.
       * @param clientCertificateStore The client cert. key store; null if not using client cert. auth.
       * @param endpointAddress The address of the tunnel end point; null if not tunneled.
       * @param strategy The proxy authentication strategy. Can be null.
       */
      public void setKey(boolean bSSL, SocketAddress address, Certificate trustedCertificate, KeyStore clientCertificateStore,
         SocketAddress endpointAddress, AuthenticationStrategy strategy)
      {
         m_bSSL = bSSL;
         m_address = address;
         m_trustedCertificate = trustedCertificate;
         m_clientCertificateStore = clientCertificateStore;
         m_endpointAddress = endpointAddress;
         m_proxyUser = null;
         m_achProxyPassword = null;

         if (strategy != null)
         {
            if (strategy.getSPNEGO() == SPNEGO_SILENT)
            {
               m_proxyUser = Undefined.VALUE;
            }
            else
            {
               PasswordAuthenticationProvider provider = strategy.getProvider();

               if (provider != null)
               {
                  PasswordAuthentication auth = provider.getPasswordAuthentication();

                  if (auth != null)
                  {
                     m_proxyUser = auth.getUserName();
                     m_achProxyPassword = auth.getPassword();
                  }
               }
            }
         }

         m_nHashCode = 0;
      }

      /**
       * @see java.lang.Object#equals(java.lang.Object)
       */
      public boolean equals(Object obj)
      {
         if (!(obj instanceof ConnectionKey))
         {
            return false;
         }

         ConnectionKey key = (ConnectionKey)obj;

         boolean bEqual = m_bSSL == key.m_bSSL &&
            m_address.equals(key.m_address) &&
            m_trustedCertificate == key.m_trustedCertificate &&
            m_clientCertificateStore == key.m_clientCertificateStore &&
            ObjUtil.equal(m_endpointAddress, key.m_endpointAddress);

         if (m_proxyUser != null)
         {
            bEqual &= m_proxyUser.equals(key.m_proxyUser) && Arrays.equals(m_achProxyPassword, key.m_achProxyPassword);
         }

         return bEqual;
      }

      /**
       * @see java.lang.Object#hashCode()
       */
      public int hashCode()
      {
         if (m_nHashCode != 0)
         {
            return m_nHashCode;
         }

         int nHashCode = m_address.hashCode() ^ ((m_bSSL) ? 33 : 0);

         if (m_clientCertificateStore != null)
         {
            nHashCode ^= m_clientCertificateStore.hashCode();
         }

         if (m_trustedCertificate != null)
         {
            nHashCode ^= m_trustedCertificate.hashCode();
         }

         if (m_endpointAddress != null)
         {
            nHashCode ^= m_endpointAddress.hashCode();
         }

         if (m_proxyUser != null)
         {
            nHashCode ^= m_proxyUser.hashCode();
            nHashCode ^= Arrays.hashCode(m_achProxyPassword);
         }

         return (m_nHashCode = (nHashCode == 0) ? 1 : nHashCode);
      }

      /**
       * @see java.lang.Object#toString()
       */
      public String toString()
      {
         StringBuilder buf = new StringBuilder(64);

         buf.append((m_bSSL) ? "https://" : "http://");
         buf.append(m_address);
         buf.append(m_trustedCertificate);
         buf.append(m_clientCertificateStore);

         return buf.toString();
      }
   }

   /**
    * Represents a cookie.
    */
   protected static class Cookie
   {
      // attributes

      /**
       * The cookie name.
       */
      protected String m_sName;

      /**
       * The cookie value.
       */
      protected String m_sValue;

      /**
       * The cookie domain.
       */
      protected String m_sDomain;

      /**
       * The cookie path.
       */
      protected String m_sPath;

      /**
       * The expiration timestamp.
       */
      protected long m_lExpiration;

      /**
       * The secure flag.
       */
      protected boolean m_bSecure;

      // associations

      /**
       * The previous cookie in a circular list.
       */
      private Cookie m_prev;

      /**
       * The next cookie in a circular list.
       */
      private Cookie m_next;

      // constructors

      /**
       * Constructs a cookie.
       * @param sName The cookie name.
       * @param sValue The cookie value.
       */
      public Cookie(String sName, String sValue)
      {
         m_sName = sName;
         m_sValue = sValue;
         m_prev = m_next = this;
      }

      // operations

      /**
       * @return The cookie name.
       */
      public String getName()
      {
         return m_sName;
      }

      /**
       * @return The cookie value.
       */
      public String getValue()
      {
         return m_sValue;
      }

      /**
       * Sets the cookie domain.
       * @param sDomain The cookie domain to set.
       */
      public void setDomain(String sDomain)
      {
         m_sDomain = sDomain;
      }

      /**
       * @return The cookie domain.
       */
      public String getDomain()
      {
         return m_sDomain;
      }

      /**
       * Sets the cookie path.
       * @param sPath The cookie path to set.
       */
      public void setPath(String sPath)
      {
         m_sPath = sPath;
      }

      /**
       * @return The cookie path.
       */
      public String getPath()
      {
         return m_sPath;
      }

      /**
       * Sets the expiration timestamp.
       * @param lExpiration The expiration timestamp to set.
       */
      public void setExpiration(long lExpiration)
      {
         m_lExpiration = lExpiration;
      }

      /**
       * @return The expiration timestamp.
       */
      public long getExpiration()
      {
         return m_lExpiration;
      }

      /**
       * Sets the secure flag.
       * @param bSecure The secure flag to set.
       */
      public void setSecure(boolean bSecure)
      {
         m_bSecure = bSecure;
      }

      /**
       * @return The secure flag.
       */
      public boolean isSecure()
      {
         return m_bSecure;
      }

      /**
       * @return The next cookie.
       */
      public Cookie getNext()
      {
         return m_next;
      }

      /**
       * Adds a cookie to the circular list.
       * @param cookie The cookie to add.
       */
      public void add(Cookie cookie)
      {
         cookie.m_next = this;
         cookie.m_prev = m_prev;
         m_prev.m_next = cookie;
         m_prev = cookie;
      }

      /**
       * Removes itself from the circular list.
       * @return True if the list is empty after the operation.
       */
      public boolean remove()
      {
         m_next.m_prev = m_prev;
         m_prev.m_next = m_next;

         return m_next == this;
      }

      /**
       * Finds a cookie that is equal to a given cookie.
       * @param cookie The cookie to look for.
       * @return The found cookie, or null if not found.
       */
      public Cookie find(Cookie cookie)
      {
         Cookie next = this;

         do
         {
            if (cookie.equals(next))
            {
               return next;
            }

            next = next.getNext();
         }
         while (next != this);

         return null;
      }

      /**
       * Checks is a cookie matches a URI.
       * @param sHost The host name (overrides the URI host).
       * @param uri The URI to match.
       */
      public boolean matches(String sHost, URI uri)
      {
         if (m_bSecure && !uri.getScheme().equalsIgnoreCase(HTTP.SCHEME_SSL))
         {
            return false;
         }

         if (m_sDomain.length() > sHost.length())
         {
            return false;
         }

         int i = sHost.length() - m_sDomain.length();

         if (!sHost.regionMatches(true, i, m_sDomain, 0, m_sDomain.length()) ||
            i > 0 && m_sDomain.charAt(0) != '.')
         {
            return false;
         }

         String sPath = uri.getRawPath();

         if (!sPath.startsWith(m_sPath) ||
            !m_sPath.equals("/") &&  sPath.length() > m_sPath.length() && sPath.charAt(m_sPath.length()) != '/')
         {
            return false;
         }

         return true;
      }

      /**
       * Gets the domain key for a given host name.
       * @param sHost The host name.
       * @return The domain key.
       */
      public static String getDomainKey(String sHost)
      {
         sHost = sHost.toLowerCase(Locale.ENGLISH);

         int i = sHost.lastIndexOf('.');

         if (i > 0)
         {
            boolean bTop = s_topDomainSet.contains(sHost.substring(i + 1));

            i = sHost.lastIndexOf('.', i - 1);

            if (i > 0 && !bTop)
            {
               i = sHost.lastIndexOf('.', i - 1);
            }

            if (i > 0)
            {
               return sHost.substring(i);
            }
         }

         return sHost;
      }

      /**
       * @see java.lang.Object#equals(java.lang.Object)
       */
      public boolean equals(Object obj)
      {
         if (!(obj instanceof Cookie))
         {
            return false;
         }

         Cookie cookie = (Cookie)obj;

         return m_sName.equalsIgnoreCase(cookie.getName()) &&
            m_sDomain.equals(cookie.getDomain()) &&
            m_sPath.equals(cookie.getPath());
      }

      /**
       * @see java.lang.Object#toString()
       */
      public String toString()
      {
         StringBuilder buf = new StringBuilder(64);

         buf.append(m_sName);
         buf.append('=');
         buf.append(m_sValue);
         buf.append("; domain=");
         buf.append(m_sDomain);
         buf.append("; path=");
         buf.append(m_sPath);

         if (m_lExpiration != Long.MIN_VALUE)
         {
            buf.append("; expires=");
            buf.append(HTTP.formatCookieDateTime(new Timestamp(m_lExpiration)));
         }

         if (m_bSecure)
         {
            buf.append("; secure");
         }

         return buf.toString();
      }
   }

   /**
    * Used for password authentication to a resource.
    */
   protected class AuthenticationStrategy
   {
      /**
       * The current user name.
       */
      protected String m_sUser;

      /**
       * The previous user name.
       */
      protected String m_sUserSaved;

      /**
       * The current user password.
       */
      protected char[] m_achPassword;

      /**
       * The previous user password.
       */
      protected char[] m_achPasswordSaved;

      /**
       * The SPNEGO protocol mode (one of the SPNEGO_* constants).
       */
      protected int m_nSPNEGO;

      /**
       * The Kerberos service name: HTTP/host.domain.ext.
       */
      protected String m_sServiceName;

      /**
       * True if the strategy is for proxy authentication.
       */
      protected boolean m_bProxy;


      // associations

      /**
       * The password provider.
       */
      protected PasswordAuthenticationProvider m_provider;

      // constructors

      /**
       * Constructs the strategy.
       * @param bProxy True if the strategy is for proxy authentication.
       */
      public AuthenticationStrategy(boolean bProxy)
      {
         m_bProxy = bProxy;
      }

      // operations

      /**
       * Sets the current user.
       *
       * @param sUser The user to set.
       */
      public void setUser(String sUser)
      {
         m_sUser = sUser;
      }

      /**
       * Gets the current user.
       *
       * @return The current user.
       */
      public String getUser()
      {
         return m_sUser;
      }

      /**
       * Gets the saved user.
       *
       * @return The saved user.
       */
      public String getUserSaved()
      {
         return m_sUserSaved;
      }

      /**
       * Sets the current password. Copies the argument.
       *
       * @param achPassword The password to set.
       */
      public void setPassword(char[] achPassword)
      {
         if (m_achPassword != null)
         {
            Arrays.fill(m_achPassword, ' ');
         }

         m_achPassword = (achPassword == null) ? null : (char[])achPassword.clone();
      }

      /**
       * Gets the current password.
       *
       * @return The current password.
       */
      public char[] getPassword()
      {
         return m_achPassword;
      }

      /**
       * Sets the password provider.
       *
       * @param provider The password provider to set.
       */
      public void setProvider(PasswordAuthenticationProvider provider)
      {
         m_provider = provider;
      }

      /**
       * Gets the password provider.
       *
       * @return The password provider.
       */
      public PasswordAuthenticationProvider getProvider()
      {
         return m_provider;
      }

      /**
       * Securely clears the current user/password.
       */
      public void clearUserPassword()
      {
         m_sUser = null;

         if (m_achPassword != null)
         {
            Arrays.fill(m_achPassword, ' ');
            m_achPassword = null;
         }
      }

      /**
       * Securely clears the saved user/password.
       */
      public void clearUserPasswordSaved()
      {
         m_sUserSaved = null;

         if (m_achPasswordSaved != null)
         {
            Arrays.fill(m_achPasswordSaved, ' ');
            m_achPasswordSaved = null;
         }
      }

      /**
       * Copies the saved user/password to the current user/password.
       */
      public void copyFromSaved()
      {
         m_sUser = m_sUserSaved;

         if (m_achPassword != null)
         {
            Arrays.fill(m_achPassword, ' ');
         }

         m_achPassword = (m_achPasswordSaved == null) ? null : (char [])m_achPasswordSaved.clone();
      }

      /**
       * Copies the current user/password to the saved user/password.
       */
      public void copyToSaved()
      {
         m_sUserSaved = m_sUser;

         if (m_achPasswordSaved != null)
         {
            Arrays.fill(m_achPasswordSaved, ' ');
         }

         m_achPasswordSaved = (m_achPassword == null) ? null : (char [])m_achPassword.clone();
      }

      /**
       * Determines whether the current user and password match the saved user and password.
       *
       * @return True if the current user and password match the saved user and password; false otherwise.
       */
      public boolean isCurrentMatchedBySaved()
      {
         return m_sUser == m_sUserSaved && Arrays.equals(m_achPassword, m_achPasswordSaved);
      }

      /**
       * Gets the SPNEGO protocol mode.
       *
       * @return One of the SPNEGO_* constants.
       */
      public int getSPNEGO()
      {
         return m_nSPNEGO;
      }

      /**
       * Sets the SPNEGO protocol mode.
       *
       * @param nSPNEGO One of the SPNEGO_* constants.
       */
      public void setSPNEGO(int nSPNEGO)
      {
         if (nSPNEGO != SPNEGO_SILENT && nSPNEGO != SPNEGO_CRED && nSPNEGO != SPNEGO_NONE)
         {
            throw new IllegalArgumentException();
         }

         m_nSPNEGO = nSPNEGO;
      }

      /**
       * Gets flag indicating that this strategy is for authenticating to proxies.
       *
       * @return True if this strategy authenticates to proxies; false otherwise.
       */
      public boolean isProxy()
      {
         return m_bProxy;
      }

      /**
       * Gets the Kerberos service principal name.
       *
       * @return The kerberos service name for the current URL.
       */
      public String getServiceName()
      {
         if (m_sServiceName == null)
         {
            InetSocketAddress address = (isProxy()) ? resolve((InetSocketAddress)m_proxy.address()) : (InetSocketAddress)m_serverAddress;

            m_sServiceName = "HTTP/" + address.getAddress().getCanonicalHostName();
         }

         return m_sServiceName;
      }

      /**
       * Clears the Kerberos service principal name, forcing it to be recomputed on the next
       * authentication attempt.
       */
      public void clearServiceName()
      {
         m_sServiceName = null;
      }

      /**
       * Parses the response headers for the authentication protocol and
       * sets the authentication information in m_sToken.
       * @return The authentication protocol, one of the AUTH_* constants.
       */
      protected int parseAuthProtocol() throws IOException
      {
         MIMEHeader header = m_responseHeaderMap.find(isProxy() ? HTTP.PROXY_AUTH_RESPONSE_HEADER : HTTP.AUTH_RESPONSE_HEADER);
         int nProtocol = AUTH_NONE;

         if (header != null)
         {
            for (int i = 0; i < header.getValueCount(); ++i)
            {
               String s = header.getValue(i).getName();

               if (nProtocol == AUTH_NONE && s.regionMatches(true, 0, HTTP.BASIC_PROTOCOL, 0, HTTP.BASIC_LENGTH))
               {
                   if (s.length() == HTTP.BASIC_LENGTH)
                   {
                      nProtocol = AUTH_BASIC;
                      m_sToken = null;
                   }
                   else if (s.charAt(HTTP.BASIC_LENGTH) == ' ')
                   {
                      nProtocol = AUTH_BASIC;
                      m_sToken = s.substring(HTTP.BASIC_LENGTH + 1);
                   }
               }
               else if (m_nSPNEGO != SPNEGO_NONE && s.regionMatches(true, 0, HTTP.SPNEGO_PROTOCOL, 0, HTTP.SPNEGO_LENGTH))
               {
                  /*
                   * Verify that the proxy supports connection pinning.
                   * (See RFC4559: http://www.ietf.org/rfc/rfc4559.txt)
                   */
                  if (!isProxy() && isHTTPProxy() && !HTTP.SCHEME_SSL.equalsIgnoreCase(m_uri.getScheme()))
                  {
                     MIMEHeader supportHeader = m_responseHeaderMap.find(HTTP.PROXY_SUPPORT_RESPONSE_HEADER);

                     if (supportHeader == null || !supportHeader.getValue().equals(HTTP.PROXY_SUPPORT_SESSION_BASED_AUTH))
                     {
                        continue;
                     }
                  }

                  if (s.length() == HTTP.SPNEGO_LENGTH)
                  {
                     nProtocol = AUTH_SPNEGO;
                     m_sToken = null;

                     break;
                  }

                  if (s.charAt(HTTP.SPNEGO_LENGTH) == ' ')
                  {
                     nProtocol = AUTH_SPNEGO;
                     m_sToken = s.substring(HTTP.SPNEGO_LENGTH + 1);

                     break;
                  }
               }
            }
         }

         return nProtocol;
      }
   }
}
TOP

Related Classes of nexj.core.util.HTTPClient$AuthenticationStrategy

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.