/*
 * @(#)AuthorizationModule.java             0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/ 
 *
 */

package org.exoplatform.common.http.client;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.net.ProtocolException;
import java.util.Hashtable;

/**
 * This module handles authentication requests. Authentication info is
 * preemptively sent if any suitable candidate info is available. If a request
 * returns with an appropriate status (401 or 407) then the necessary info is
 * sought from the AuthenticationInfo class.
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 */
class AuthorizationModule implements HTTPClientModule
{
   /**
    * This holds the current Proxy-Authorization-Info for each HTTPConnection
    */
   private static Hashtable proxy_cntxt_list = new Hashtable();

   /**
    * a list of deferred authorization retries (used with
    * Response.retryRequest())
    */
   private static Hashtable deferred_auth_list = new Hashtable();

   /** counters for challenge and auth-info lists */
   private int auth_lst_idx, prxy_lst_idx, auth_scm_idx, prxy_scm_idx;

   /** the last auth info sent, if any */
   private AuthorizationInfo auth_sent;

   private AuthorizationInfo prxy_sent;

   /** is the info in auth_sent a preemtive guess or the result of a 4xx */
   private boolean auth_from_4xx;

   private boolean prxy_from_4xx;

   /** guard against bugs on both our side and the server side */
   private int num_tries;

   /** used for deferred authoriation retries */
   private Request saved_req;

   private Response saved_resp;

   private static final Log LOG = ExoLogger.getLogger("exo.ws.commons.AuthorizationModule");

   // Constructors

   /**
    * Initialize counters for challenge and auth-info lists.
    */
   AuthorizationModule()
   {
      auth_lst_idx = 0;
      prxy_lst_idx = 0;
      auth_scm_idx = 0;
      prxy_scm_idx = 0;

      auth_sent = null;
      prxy_sent = null;

      auth_from_4xx = false;
      prxy_from_4xx = false;

      num_tries = 0;
      saved_req = null;
      saved_resp = null;
   }

   // Methods

   /**
    * Invoked by the HTTPClient.
    */
   public int requestHandler(Request req, Response[] resp) throws IOException, AuthSchemeNotImplException
   {
      HTTPConnection con = req.getConnection();
      AuthorizationHandler auth_handler = AuthorizationInfo.getAuthHandler();
      AuthorizationInfo guess;
      NVPair[] hdrs = req.getHeaders();
      int rem_idx = -1;

      // check for retries

      HttpOutputStream out = req.getStream();
      if (out != null && deferred_auth_list.get(out) != null)
      {
         copyFrom((AuthorizationModule)deferred_auth_list.remove(out));
         req.copyFrom(saved_req);

         if (LOG.isDebugEnabled())
            LOG.debug("Handling deferred auth challenge");

         handle_auth_challenge(req, saved_resp);

         if (auth_sent != null)
         {
            if (LOG.isDebugEnabled())
               LOG.debug("Sending request with Authorization '" + auth_sent + "'");

         }
         else
         {

            if (LOG.isDebugEnabled())
               LOG.debug("Sending request with Proxy-Authorization '" + prxy_sent + "'");
         }
         return REQ_RESTART;
      }

      // Preemptively send proxy authorization info

      Proxy : if (con.getProxyHost() != null && !prxy_from_4xx)
      {
         // first remove any Proxy-Auth header that still may be around

         for (int idx = 0; idx < hdrs.length; idx++)
         {
            if (hdrs[idx].getName().equalsIgnoreCase("Proxy-Authorization"))
            {
               rem_idx = idx;
               break;
            }
         }
         Hashtable proxy_auth_list = Util.getList(proxy_cntxt_list, con.getContext());
         guess = (AuthorizationInfo)proxy_auth_list.get(con.getProxyHost() + ":" + con.getProxyPort());
         if (guess == null)
            break Proxy;

         if (auth_handler != null)
         {
            try
            {
               guess = auth_handler.fixupAuthInfo(guess, req, null, null);
            }
            catch (AuthSchemeNotImplException asnie)
            {
               break Proxy;
            }
            if (guess == null)
               break Proxy;
         }

         if (rem_idx == -1) // add proxy-auth header
         {
            rem_idx = hdrs.length;
            hdrs = Util.resizeArray(hdrs, rem_idx + 1);
            req.setHeaders(hdrs);
         }

         hdrs[rem_idx] = new NVPair("Proxy-Authorization", guess.toString());
         rem_idx = -1;

         prxy_sent = guess;
         prxy_from_4xx = false;

         if (LOG.isDebugEnabled())
            LOG.debug("Preemptively sending Proxy-Authorization '" + guess + "'");

      }
      if (rem_idx >= 0)
      {
         System.arraycopy(hdrs, rem_idx + 1, hdrs, rem_idx, hdrs.length - rem_idx - 1);
         hdrs = Util.resizeArray(hdrs, hdrs.length - 1);
         req.setHeaders(hdrs);
      }

      // Preemptively send authorization info

      rem_idx = -1;
      Auth : if (!auth_from_4xx)
      {
         // first remove any Auth header that still may be around

         for (int idx = 0; idx < hdrs.length; idx++)
         {
            if (hdrs[idx].getName().equalsIgnoreCase("Authorization"))
            {
               rem_idx = idx;
               break;
            }
         }

         // now try and guess whether we need to send auth info

         guess = AuthorizationInfo.findBest(req);
         if (guess == null)
            break Auth;

         if (auth_handler != null)
         {
            try
            {
               guess = auth_handler.fixupAuthInfo(guess, req, null, null);
            }
            catch (AuthSchemeNotImplException asnie)
            {
               break Auth;
            }
            if (guess == null)
               break Auth;
         }

         if (rem_idx == -1) // add auth header
         {
            rem_idx = hdrs.length;
            hdrs = Util.resizeArray(hdrs, rem_idx + 1);
            req.setHeaders(hdrs);
         }

         hdrs[rem_idx] = new NVPair("Authorization", guess.toString());
         rem_idx = -1;

         auth_sent = guess;
         auth_from_4xx = false;

         if (LOG.isDebugEnabled())
            LOG.debug("Preemptively sending Authorization '" + guess + "'");

      }
      if (rem_idx >= 0)
      {
         System.arraycopy(hdrs, rem_idx + 1, hdrs, rem_idx, hdrs.length - rem_idx - 1);
         hdrs = Util.resizeArray(hdrs, hdrs.length - 1);
         req.setHeaders(hdrs);
      }

      return REQ_CONTINUE;
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void responsePhase1Handler(Response resp, RoRequest req) throws IOException
   {
      /*
       * If auth info successful update path list. Note: if we preemptively sent
       * auth info we don't actually know if it was necessary. Therefore we don't
       * update the path list in this case; this prevents it from being
       * contaminated. If the info was necessary, then the next time we access
       * this resource we will again guess the same info and send it.
       */
      if (resp.getStatusCode() != 401 && resp.getStatusCode() != 407)
      {
         if (auth_sent != null && auth_from_4xx)
         {
            try
            {
               AuthorizationInfo.getAuthorization(auth_sent, req, resp, false).addPath(req.getRequestURI());
            }
            catch (AuthSchemeNotImplException asnie)
            {
               if (LOG.isTraceEnabled())
               {
                  LOG.trace("An exception occurred: " + asnie.getMessage());
               }
            }
         }

         // reset guard if not an auth challenge
         num_tries = 0;
      }

      auth_from_4xx = false;
      prxy_from_4xx = false;

      if (resp.getHeader("WWW-Authenticate") == null)
      {
         auth_lst_idx = 0;
         auth_scm_idx = 0;
      }

      if (resp.getHeader("Proxy-Authenticate") == null)
      {
         prxy_lst_idx = 0;
         prxy_scm_idx = 0;
      }
   }

   /**
    * Invoked by the HTTPClient.
    */
   public int responsePhase2Handler(Response resp, Request req) throws IOException, AuthSchemeNotImplException
   {
      // Let the AuthHandler handle any Authentication headers.

      AuthorizationHandler h = AuthorizationInfo.getAuthHandler();
      if (h != null)
         h.handleAuthHeaders(resp, req, auth_sent, prxy_sent);

      // handle 401 and 407 response codes

      int sts = resp.getStatusCode();
      switch (sts)
      {
         case 401 : // Unauthorized
         case 407 : // Proxy Authentication Required

            // guard against infinite retries due to bugs

            num_tries++;
            if (num_tries > 10)
               throw new ProtocolException("Bug in authorization handling: server refused the given info 10 times");

            // defer handling if a stream was used

            if (req.getStream() != null)
            {
               if (!HTTPConnection.deferStreamed)
               {
                  if (LOG.isDebugEnabled())
                     LOG.debug("Status " + sts + " not handled - request has an output stream");

                  return RSP_CONTINUE;
               }

               saved_req = (Request)req.clone();
               saved_resp = (Response)resp.clone();
               deferred_auth_list.put(req.getStream(), this);

               req.getStream().reset();
               resp.setRetryRequest(true);

               if (LOG.isDebugEnabled())
                  LOG.debug("Handling of status " + sts + " deferred because an output stream was used");

               return RSP_CONTINUE;
            }

            // handle the challenge

            if (LOG.isDebugEnabled())
               LOG.debug("Handling status: " + sts + " " + resp.getReasonLine());

            handle_auth_challenge(req, resp);

            // check for valid challenge

            if (auth_sent != null || prxy_sent != null)
            {
               try
               {
                  resp.getInputStream().close();
               }
               catch (IOException ioe)
               {
                  if (LOG.isTraceEnabled())
                  {
                     LOG.trace("An exception occurred: " + ioe.getMessage());
                  }
               }

               if (auth_sent != null)
               {
                  if (LOG.isDebugEnabled())
                  {
                     LOG.debug("Resending request with Authorization '" + auth_sent + "'");
                  }
               }
               else
               {
                  if (LOG.isDebugEnabled())
                  {
                     LOG.debug("Resending request with Proxy-Authorization '" + prxy_sent + "'");
                  }
               }

               return RSP_REQUEST;
            }

            if (req.getStream() != null)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.debug("Status " + sts + " not handled - request has an output stream");
               }
            }
            else
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.debug("No Auth Info found - status " + sts + " not handled");
               }
            }
            return RSP_CONTINUE;

         default :

            return RSP_CONTINUE;
      }
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void responsePhase3Handler(Response resp, RoRequest req)
   {
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void trailerHandler(Response resp, RoRequest req) throws IOException
   {
      // Let the AuthHandler handle any Authentication headers.

      AuthorizationHandler h = AuthorizationInfo.getAuthHandler();
      if (h != null)
         h.handleAuthTrailers(resp, req, auth_sent, prxy_sent);
   }

   /**
    *
    */
   private void handle_auth_challenge(Request req, Response resp) throws AuthSchemeNotImplException, IOException
   {
      // handle WWW-Authenticate

      int[] idx_arr = {auth_lst_idx, // hack to pass by ref
         auth_scm_idx};
      auth_sent = setAuthHeaders(resp.getHeader("WWW-Authenticate"), req, resp, "Authorization", idx_arr, auth_sent);
      if (auth_sent != null)
      {
         auth_from_4xx = true;
         auth_lst_idx = idx_arr[0];
         auth_scm_idx = idx_arr[1];
      }
      else
      {
         auth_lst_idx = 0;
         auth_scm_idx = 0;
      }

      // handle Proxy-Authenticate

      idx_arr[0] = prxy_lst_idx; // hack to pass by ref
      idx_arr[1] = prxy_scm_idx;
      prxy_sent =
         setAuthHeaders(resp.getHeader("Proxy-Authenticate"), req, resp, "Proxy-Authorization", idx_arr, prxy_sent);
      if (prxy_sent != null)
      {
         prxy_from_4xx = true;
         prxy_lst_idx = idx_arr[0];
         prxy_scm_idx = idx_arr[1];
      }
      else
      {
         prxy_lst_idx = 0;
         prxy_scm_idx = 0;
      }

      if (prxy_sent != null)
      {
         HTTPConnection con = req.getConnection();
         Util.getList(proxy_cntxt_list, con.getContext()).put(con.getProxyHost() + ":" + con.getProxyPort(), prxy_sent);
      }

      // check for headers

      if (auth_sent == null && prxy_sent == null && resp.getHeader("WWW-Authenticate") == null
         && resp.getHeader("Proxy-Authenticate") == null)
      {
         if (resp.getStatusCode() == 401)
            throw new ProtocolException("Missing WWW-Authenticate header");
         else
            throw new ProtocolException("Missing Proxy-Authenticate header");
      }
   }

   /**
    * Handles authentication requests and sets the authorization headers. It
    * tries to retrieve the neccessary parameters from AuthorizationInfo, and
    * failing that calls the AuthHandler. Handles multiple authentication
    * headers.
    * @param auth_str the authentication header field returned by the server.
    * @param req the Request used
    * @param resp the full Response received
    * @param header the header name to use in the new headers array.
    * @param idx_arr an array of indicies holding the state of where we are when
    *          handling multiple authorization headers.
    * @param prev the previous auth info sent, or null if none
    * @return the new credentials, or null if none found
    * @exception ProtocolException if <var>auth_str</var> is null.
    * @exception AuthSchemeNotImplException if thrown by the AuthHandler.
    * @exception IOException if thrown by the AuthHandler.
    */
   private AuthorizationInfo setAuthHeaders(String auth_str, Request req, RoResponse resp, String header,
      int[] idx_arr, AuthorizationInfo prev) throws ProtocolException, AuthSchemeNotImplException, IOException
   {
      if (auth_str == null)
         return null;

      // get the list of challenges the server sent
      AuthorizationInfo[] challenges = AuthorizationInfo.parseAuthString(auth_str, req, resp);

      if (LOG.isDebugEnabled())
      {
         LOG.debug("Parsed " + challenges.length + " challenges:");
         for (int idx = 0; idx < challenges.length; idx++)
            LOG.debug("AuthM: Challenge " + challenges[idx]);
      }

      if (challenges.length == 0)
         return null;

      /*
       * some servers expect a 401 to invalidate sent credentials. However, only
       * do this for Basic scheme (because e.g. digest "stale" handling will fail
       * otherwise)
       */
      if (prev != null && prev.getScheme().equalsIgnoreCase("Basic"))
      {
         for (int idx = 0; idx < challenges.length; idx++)
            if (prev.getRealm().equals(challenges[idx].getRealm())
               && prev.getScheme().equalsIgnoreCase(challenges[idx].getScheme()))
               AuthorizationInfo.removeAuthorization(prev, req.getConnection().getContext());
      }

      AuthorizationInfo credentials = null;
      AuthorizationHandler auth_handler = AuthorizationInfo.getAuthHandler();

      // try next auth challenge in list
      while (credentials == null && idx_arr[0] != -1 && idx_arr[0] < challenges.length)
      {
         credentials = AuthorizationInfo.getAuthorization(challenges[idx_arr[0]], req, resp, false);
         if (auth_handler != null && credentials != null)
            credentials = auth_handler.fixupAuthInfo(credentials, req, challenges[idx_arr[0]], resp);
         if (++idx_arr[0] == challenges.length)
            idx_arr[0] = -1;
      }

      // if we don't have any credentials then prompt the user
      if (credentials == null)
      {
         for (int idx = 0; idx < challenges.length; idx++)
         {
            if (idx_arr[1] >= challenges.length)
               idx_arr[1] = 0;

            try
            {
               credentials = AuthorizationInfo.queryAuthHandler(challenges[idx_arr[1]], req, resp);
               break;
            }
            catch (AuthSchemeNotImplException asnie)
            {
               if (idx == challenges.length - 1)
                  throw asnie;
            }
            finally
            {
               idx_arr[1]++;
            }
         }
      }

      // if we still don't have any credentials then give up
      if (credentials == null)
         return null;

      // find auth info
      int auth_idx;
      NVPair[] hdrs = req.getHeaders();
      for (auth_idx = 0; auth_idx < hdrs.length; auth_idx++)
      {
         if (hdrs[auth_idx].getName().equalsIgnoreCase(header))
            break;
      }

      // add credentials to headers
      if (auth_idx == hdrs.length)
      {
         hdrs = Util.resizeArray(hdrs, auth_idx + 1);
         req.setHeaders(hdrs);
      }
      hdrs[auth_idx] = new NVPair(header, credentials.toString());

      return credentials;
   }

   private void copyFrom(AuthorizationModule other)
   {
      this.auth_lst_idx = other.auth_lst_idx;
      this.prxy_lst_idx = other.prxy_lst_idx;
      this.auth_scm_idx = other.auth_scm_idx;
      this.prxy_scm_idx = other.prxy_scm_idx;

      this.auth_sent = other.auth_sent;
      this.prxy_sent = other.prxy_sent;

      this.auth_from_4xx = other.auth_from_4xx;
      this.prxy_from_4xx = other.prxy_from_4xx;

      this.num_tries = other.num_tries;

      this.saved_req = other.saved_req;
      this.saved_resp = other.saved_resp;
   }
}
