/*
 * @(#)AuthorizationInfo.java				0.2-2 23/03/1997
 *
 *  This file is part of the HTTPClient package 
 *  Copyright (C) 1996,1997  Ronald Tschalaer
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library 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
 *  Library General Public License for more details.
 *
 *  You should have received a copy of the GNU Library 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
 *  Ronald.Tschalaer@psi.ch
 *
 */

package HTTPClient;


import java.awt.*;
import java.util.*;
import java.net.ProtocolException;

/**
 * Holds the information for an authorization response.
 *
 * @version	0.2 (bug fix 2)  23/03/1997
 * @author	Ronald Tschal&auml;r
 */

public class AuthorizationInfo
{
    // class fields

    /** Holds the list of authorization info structures */
    private static Hashtable     AuthList = new Hashtable();

    /** A pointer to the handler to be called when we need authorization info */
    private static AuthorizationHandler
				 AuthHandler = new MyAuthHandler();


    // the instance oriented stuff

    /** the host */
    String Host;

    /** the port */
    int Port;

    /** the scheme. (e.g. "basic") */
    String Scheme;

    /** the realm */
    String Realm;

    /** the string used for the "basic" authorization scheme */
    String basic_cookie;

    /** any parameters */
    Vector auth_params = new Vector();


    // Constructors

    /**
     * Creates an empty info structure.
     */
    AuthorizationInfo()
    {}

    /**
     * Creates an new info structure for the specified host and port.
     * @param host   the host
     * @param port   the port
     */
    AuthorizationInfo(String host, int port)
    {
	this.Host = host;
	this.Port = port;
    }

    /**
     * Creates a new info structure for the specified host and port with the
     * specified scheme, realm, params. The "basic" cookie is set to null.
     * @param host   the host
     * @param port   the port
     * @param scheme the scheme
     * @param realm  the realm
     * @param params the parameters as an array of name/value pairs
     */
    public AuthorizationInfo(String host, int port, String scheme,
			     String realm, NVPair params[])
    {
	this.Scheme       = scheme.trim();
	this.Host         = host.trim();
	this.Port         = port;
	this.Realm        = realm.trim();
	this.basic_cookie = null;
	this.auth_params.ensureCapacity(params.length);
	for (int idx=0; idx < params.length; idx++)
	    this.auth_params.addElement(params[idx]);
    }

    /**
     * Creates a new info structure for the specified host and port with the
     * specified scheme, realm and "basic" cookie. The params is set to
     * a zero-length array.
     * @param host   the host
     * @param port   the port
     * @param scheme the scheme
     * @param realm  the realm
     * @param cookie the encoded username/password for the "basic" scheme
     */
    public AuthorizationInfo(String host, int port, String scheme,
			     String realm, String cookie)
    {
	this.Scheme       = scheme.trim();
	this.Host         = host.trim();
	this.Port         = port;
	this.Realm        = realm.trim();
	if (cookie != null)
	    this.basic_cookie = cookie.trim();
	else
	    this.basic_cookie = null;
    }

    /**
     * Creates a new copy of the given AuthorizationInfo.
     * @param templ the info to copy
     */
    AuthorizationInfo(AuthorizationInfo templ)
    {
	this.Scheme       = templ.Scheme;
	this.Host         = templ.Host;
	this.Port         = templ.Port;
	this.Realm        = templ.Realm;
	this.basic_cookie = templ.basic_cookie;
	this.auth_params  = (Vector) templ.auth_params.clone();
    }


    // Class Methods

    /**
     * Set's the authorization handler. This handler is called whenever
     * the server requests authorization and no entry for the requested
     * scheme and realm can be found in the list. The handler must implement
     * the AuthorizationHandler interface.
     * <BR>If no handler is set then a default handler is used. This handler
     * currently only handles the "basic" scheme and brings up a popup which
     * prompts for the username and password.
     *
     * @see    AuthorizationHandler
     * @param  handler the new authorization handler
     * @return the old authorization handler
     */
    public static AuthorizationHandler
		    setAuthHandler(AuthorizationHandler handler)
    {
	AuthorizationHandler tmp = AuthHandler;
	AuthHandler        = handler;

	return tmp;
    }

    /**
     * Queries the AuthHandler for authorization info. It also adds this
     * info to the list.
     *
     * @param  info  any info needed by the AuthHandler; at a minimum the
     *               host, scheme and realm should be set.
     * @return a structure containing the requested info, or null if either
     *	       no AuthHandler is set or the user canceled the request.
     * @exception AuthTypeNotImplementedException if this is thrown by
     *                                            the AuthHandler.
     */
    static AuthorizationInfo queryAuthHandler(AuthorizationInfo info)
	throws AuthTypeNotImplementedException
    {
	if (AuthHandler == null)
	    return null;
	else
	{
	    AuthorizationInfo new_info = AuthHandler.getAuthorization(info);
	    if (new_info != null)
		addAuthorization(new_info);
	    return new_info;
	}
    }

    /**
     * searches for the authorization info using the host, port, scheme and
     * realm in the given info struct. If not found it queries the
     * AuthHandler (if set).
     *
     * @param  auth_info    the AuthorizationInfo
     * @param  query_auth_h if true, query the auth-handler if no info found.
     * @return a pointer to the authorization data or null if not found
     * @exception AuthTypeNotImplemented If thrown by the AuthHandler.
     */
    static synchronized AuthorizationInfo
	    getAuthorization(AuthorizationInfo auth_info, boolean query_auth_h)
	throws AuthTypeNotImplementedException
    {
	AuthorizationInfo new_info =
	    (AuthorizationInfo) AuthList.get(auth_info);

	if (new_info == null  &&  query_auth_h)
	    new_info = queryAuthHandler(auth_info);

	return new_info;
    }

    /**
     * searches for the authorization info given a host, port, scheme and
     * realm. Queries the AuthHandler if not found in list.
     *
     * @param  host         the host
     * @param  port         the port
     * @param  scheme       the scheme
     * @param  realm        the realm
     * @param  query_auth_h if true, query the auth-handler if no info found.
     * @return a pointer to the authorization data or null if not found
     * @exception AuthTypeNotImplemented If thrown by the AuthHandler.
     */
    static AuthorizationInfo getAuthorization(String host, int port,
					      String scheme, String realm,
					      boolean query_auth_h)
	throws AuthTypeNotImplementedException
    {
	return getAuthorization(new AuthorizationInfo(host.trim(), port,
				scheme.trim(), realm.trim(), (String) null),
				query_auth_h);
    }

    /**
     * Add's an authorization entry to the list. If an entry for the
     * specified scheme and realm already exists then its cookie and
     * params are replaced with the new data.
     *
     * @param auth_info the AuthorizationInfo to add
     */
    static void addAuthorization(AuthorizationInfo auth_info)
    {
	AuthList.put(auth_info, auth_info);
    }

    /**
     * Add's an authorization entry to the list. If an entry for the
     * specified scheme and realm already exists then its cookie and
     * params are replaced with the new data.
     *
     * @param host   the host
     * @param port   the port
     * @param scheme the scheme
     * @param realm  the realm
     * @param cookie the string used for the "basic" authorization scheme
     * @param params an array of name/value pairs of parameters
     */
    public static void addAuthorization(String host, int port, String scheme,
					String realm, String cookie,
					NVPair params[])
    {
	Vector vparams;

	if (params != null)
	{
	    vparams = new Vector(params.length);
	    for (int idx=0; idx < params.length; idx++)
		vparams.addElement(params[idx]);
	}
	else
	    vparams = new Vector();

	addAuthorization(host, port, scheme, realm, cookie, vparams);
    }


    /**
     * Add's an authorization entry to the list. If an entry for the
     * specified scheme and realm already exists then it's cookie and
     * params are overwritten with the new data.
     *
     * @param host   the host
     * @param port   the port
     * @param scheme the scheme
     * @param realm  the realm
     * @param cookie the string used for the "basic" authorization scheme
     * @param params a vector of name/value pairs of parameters
     */
    static synchronized void addAuthorization(String host, int port,
					      String scheme, String realm,
					      String cookie, Vector params)
    {
	AuthorizationInfo auth =
	    new AuthorizationInfo(host, port, scheme, realm, cookie);
	if (params != null)
	    auth.auth_params = params;

	AuthList.put(auth, auth);
    }

    /**
     * Add's an authorization entry for the "basic" authorization scheme to
     * the list. If an entry already exists for the "basic" scheme and the
     * specified realm then it is overwritten.
     *
     * @param host   the host
     * @param port   the port
     * @param realm the realm
     * @param user  the username
     * @param passw the password
     */
    public static void addBasicAuthorization(String host, int port,
					     String realm, String user,
					     String passw)
    {
	addAuthorization(host, port, "Basic", realm,
			 Codecs.base64Encode(user + ":" + passw),
			 (NVPair[]) null);
    }


    /**
     * Parses the authentication challenge(s) into an array of new info
     * structures for the specified host and port.
     *
     * @param host   the host
     * @param port   the port
     * @param challenge a string containing authentication info. This must
     *                  have the same format as value part of a
     *                  WWW-authenticate response header field, and may
     *                  contain multiple authentication challenges.
     * @exception ProtocolException if any error during the parsing occurs.
     */
    static AuthorizationInfo[] parseAuthString(String host, int port,
					       String challenge)
	    throws ProtocolException
    {
	int    beg = 0,
	       end = 0;
	char[] buf = challenge.toCharArray();
	int    len = buf.length;

	AuthorizationInfo auth_arr[] = new AuthorizationInfo[0],
			  curr;

	while (true)			// get all challenges
	{
	    // get scheme
	    beg  = skipSpace(buf, beg);
	    if (beg == len)  break;

	    end         = findSpace(buf, beg+1);
	    curr        = new AuthorizationInfo(host, port);
	    curr.Scheme = challenge.substring(beg, end);

	    // get realm
	    beg = end;
	    beg = skipSpace(buf, beg);
	    if (!challenge.regionMatches(true, beg, "realm", 0, 5))
		throw new ProtocolException("Bad Authorization header format: "
					    + challenge + "\nExpected \"realm\""
					    + " at position " + beg);

	    beg += 5;
	    beg = skipSpace(buf, beg);
	    if (beg == len  ||  buf[beg] != '=')
		throw new ProtocolException("Bad Authorization header format: "
					    + challenge + "\nExpected \"=\" at"
					    + " position " + beg);

	    beg++;
	    beg = skipSpace(buf, beg);
	    if (beg == len  ||  buf[beg] != '\"')
		throw new ProtocolException("Bad Authorization header format: "
					    + challenge + "\nExpected \'\\\"\'"
					    + " at position " + beg);

	    beg++;
	    end = challenge.indexOf('\"', beg);
	    if (end == -1)
		throw new ProtocolException("Bad Authorization header format: "
					    + challenge + "\nClosing \'\\\"\' "
					    + "for position " + beg + " not "
					    + "found");

	    curr.Realm = challenge.substring(beg, end);

	    // get optional auth-parameters
	    while (true)
	    {
		beg = skipSpace(buf, end+1);	// find ","
		if (beg == len)  break;
		if (buf[beg] != ',')
		    throw new ProtocolException("Bad Authorization header " +
						"format: " + challenge +
						"\nExpected \",\" at position" +
						beg);

		beg = skipSpace(buf, beg+1);	// find param name
		if (beg == len)  break;
		if (buf[beg] == ',')	// skip empty params
		{
		    end = beg;
		    continue;
		}

		NVPair param  = new NVPair();
		int    pstart = beg;

		end = findSpace(buf, beg+1);	// extract name
		param.name = challenge.substring(beg, end).trim();

		beg = skipSpace(buf, end);	// find "="
		if (beg == len)
		    throw new ProtocolException("Bad Authorization header " +
						"format: " + challenge +
						"\nUnexpected EOL after token" +
						" at position " + (end-1));
		if (buf[beg] != '=')  // It's not a param, but another challenge
		{
		    beg = pstart;
		    break;
		}

		beg = skipSpace(buf, beg+1);
		if (buf[beg] != '\"')
		    throw new ProtocolException("Bad Authorization header " +
						"format: " + challenge +
						"\nExpected \'\\\"\' at " +
						"position " + beg);
		beg++;
		end = challenge.indexOf('\"', beg);
		if (end == -1)
		    throw new ProtocolException("Bad Authorization header " +
						"format: " + challenge +
						"\nClosing \'\\\"\' for " +
						"position " + beg + " not " +
						"found");
		param.value = challenge.substring(beg, end);

		curr.auth_params.addElement(param);
	    }

	    auth_arr = Util.resizeArray(auth_arr, auth_arr.length+1);
	    auth_arr[auth_arr.length-1] = curr;
	}

	return auth_arr;
    }

    /**
     * returns the position of the first non-space character in a char array
     * starting a position pos.
     *
     * @param str the char array
     * @param pos the position to start looking
     * @return the position of the first non-space character
     */
    private static int skipSpace(char[] str, int pos)
    {
	int len = str.length;
	while (pos < len  &&  Character.isSpace(str[pos]))  pos++;
	return pos;
    }

    /**
     * returns the position of the first space character in a char array
     * starting a position pos.
     *
     * @param str the char array
     * @param pos the position to start looking
     * @return the position of the first space character
     */
    private static int findSpace(char[] str, int pos)
    {
	int len = str.length;
	while (pos < len  &&  !Character.isSpace(str[pos]))  pos++;
	return pos;
    }


    // Instance Methods 

    /**
     * Get the host.
     * @return a string containing the host name.
     */
    public final String getHost()
    {
	return Host;
    }


    /**
     * Get the port.
     * @return an int containing the port number.
     */
    public final int getPort()
    {
	return Port;
    }


    /**
     * Get the scheme.
     * @return a string containing the scheme.
     */
    public final String getScheme()
    {
	return Scheme;
    }


    /**
     * Get the realm.
     * @return a string containing the realm.
     */
    public final String getRealm()
    {
	return Realm;
    }


    /**
     * Get the authorization parameters.
     * @return an array of name/value pairs.
     */
    public final NVPair[] getParams()
    {
	NVPair[] params = new NVPair[auth_params.size()];
	auth_params.copyInto(params);
	return params;
    }


    /**
     * Constructs a string containing the authorization info. The format
     * is that of the http header Authenticate.
     * @return a String containing all info.
     */
    public String toString()
    {
	StringBuffer field = new StringBuffer(100);

	field.append(Scheme);
	field.append(" ");

	if (Scheme.equalsIgnoreCase("basic"))
	{
	    field.append(basic_cookie);
	    field.append("\r\n");
	}
	else
	{
	    field.append("realm=\"");
	    field.append(Realm);
	    field.append('\"');

	    Enumeration params = auth_params.elements();
	    while (params.hasMoreElements())
	    {
		NVPair param = (NVPair) params.nextElement();
		field.append(',');
		field.append(param.name);
		field.append("=\"");
		field.append(param.value);
		field.append('\"');
	    }
	    field.append("\r\n");
	}

	return field.toString();
    }


    /**
     * Produces a hash code based on Host, Scheme and Realm. Port is not
     * included for simplicity (and because it probably won't make much
     * difference). Used in the AuthorizationInfo.AuthList hash table.
     *
     * @return the hash code
     */
    public int hashCode()
    {
	return (Host.toLowerCase()+Scheme.toLowerCase()+Realm).hashCode();
    }

    /**
     * Two AuthorizationInfos are considered equal if their Host, Port,
     * Scheme and Realm match. Used in the AuthorizationInfo.AuthList hash
     * table.
     *
     * @param obj another AuthorizationInfo against which this one is
     *            to be compared.
     * @return true if they match in the above mentioned fields; false
     *              otherwise.
     */
    public boolean equals(Object obj)
    {
	if ((obj != null)  &&  (obj instanceof AuthorizationInfo))
	{
	    AuthorizationInfo auth = (AuthorizationInfo) obj;
	    if (Host.equalsIgnoreCase(auth.Host)  &&
		(Port == auth.Port)  &&
		Scheme.equalsIgnoreCase(auth.Scheme)  &&
		Realm.equals(auth.Realm))
		    return true;
	}
	return false;
    }
}


/**
 * A simple authorization handler that throws up a message box requesting
 * both a username and password. This is default authorization handler.
 * Currently only handles the authorization type "Basic" and "SOCKS5"
 * (used for the SocksClient and not part of HTTP per se).
 */

class MyAuthHandler implements AuthorizationHandler
{
    /** debug */
    private static final boolean debug = false;


    /**
     * returns the requested authorization, or null if none was given.
     *
     * @param challenge the parsed challenge from the server
     * @param Node      the node the challenge came from
     * @return a structure containing the necessary authorization info,
     *         or null
     * @exception AuthTypeNotImplementedException if the authorization scheme
     *             in the challenge cannot be handled.
     */
    public AuthorizationInfo getAuthorization(AuthorizationInfo challenge)
		    throws AuthTypeNotImplementedException
    {
	AuthorizationInfo cred;


	if (debug)
	    System.err.println("Requesting Authorization for host " + 
				challenge.getHost()+":"+challenge.getPort() +
				"; scheme: " + challenge.getScheme() +
				"; realm: " + challenge.getRealm());


	if (challenge.getScheme().equalsIgnoreCase("basic")  ||
	    challenge.getScheme().equalsIgnoreCase("SOCKS5"))
	{
	    BasicAuthBox inp;

	    if (challenge.getScheme().equalsIgnoreCase("basic"))
	    {
		inp = new BasicAuthBox("Enter username/password for realm " +
					challenge.getRealm(),
					" on host " + challenge.getHost());
	    }
	    else
	    {
		inp = new BasicAuthBox("Enter username/password for SOCKS " +
					"server on host ",
					challenge.getHost());
	    }

	    NVPair answer = inp.getInput();

	    if (answer == null)
		cred = null;
	    else
	    {
		if (challenge.getScheme().equalsIgnoreCase("basic"))
		{
		    cred = new AuthorizationInfo(challenge.getHost(),
						 challenge.getPort(),
						 challenge.getScheme(),
						 challenge.getRealm(),
						 Codecs.base64Encode(
							answer.name + ":" +
							answer.value));
		}
		else
		{
		    NVPair[] upwd = { answer };
		    cred = new AuthorizationInfo(challenge.getHost(),
						 challenge.getPort(),
						 challenge.getScheme(),
						 challenge.getRealm(),
						 upwd);
		}
	    }
	}
	else	// unimplemented authorization scheme
	{
	    throw new AuthTypeNotImplementedException(challenge.Scheme);
	}

	if (debug) System.err.println("Got Authorization");

	return cred;
    }
}


/**
 * A simple popup that request username and password used for the "basic"
 * authorization scheme.
 */
class BasicAuthBox extends Frame
{
    private static final String title = "Authorization Request";
    private TextField		user;
    private TextField		pass;
    private int                 done;
    private static final int    NOPE = 0, OK = 1, CANCEL = -1;


    /**
     * Constructs the popup with two lines of text above the input fields
     *
     * @param line1 the first line
     * @param line2 the second line
     */
    BasicAuthBox(String line1, String line2)
    {
	super(title);

	setLayout(new BorderLayout());

	Panel p = new Panel();
	p.setLayout(new GridLayout(2,1));
	p.add(new Label(line1));
	p.add(new Label(line2));
	add("North", p);

	p = new Panel();
	p.setLayout(new GridLayout(2,1));
	p.add(new Label("Username:"));
	p.add(new Label("Password:"));
	add("West", p);
	p = new Panel();
	p.setLayout(new GridLayout(2,1));
	p.add(user = new TextField(30));
	p.add(pass = new TextField(30));
	pass.setEchoCharacter('*');
	add("East", p);

	p = new Panel();
	p.setLayout(new GridLayout(1,3));
	p.add(new Button("OK"));
	p.add(new Button("Clear"));
	p.add(new Button("Cancel"));
	Panel pp = new Panel();
	pp.add(p);
	add("South", pp);

	pack();
	setResizable(false);
    }


    /**
     * our event handler
     */
    public boolean action(Event event, Object obj)
    {
	if (obj.equals("OK"))
	{
	    done = OK;
	    return true;
	} 

	if (obj.equals("Clear"))
	{
	    user.setText("");
	    pass.setText("");
	    return true;
	} 

	if (obj.equals("Cancel"))
	{
	    done = CANCEL;
	    return true;
	} 

	return super.action(event, obj);
    }

    /**
     * the method called by MyAuthHandler.
     *
     * @return the username/password pair
     */
    NVPair getInput()
    {
	show();

	done = NOPE;
	while (done == NOPE)
	{
	    try { Thread.sleep(100); } catch (InterruptedException e) { }
	}

	dispose();

	if (done == CANCEL)
	    return null;
	else
	    return new NVPair(user.getText(), pass.getText());
    }
}

