/*
 * @(#)DefaultAuthHandler.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 HTTPClient;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.DataInputStream;
import java.io.InputStreamReader;
import java.util.Vector;
import java.util.StringTokenizer;
//HS:
import java.util.Random;

import java.awt.Frame;
import java.awt.Panel;
import java.awt.Label;
import java.awt.Button;
import java.awt.Dimension;
import java.awt.TextField;
import java.awt.GridLayout;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;

/**
 * This class is the default authorization handler. It currently handles the
 * authentication schemes "Basic", "Digest", and "SOCKS5" (used for the
 * SocksClient and not part of HTTP per se).
 *
 * <P>By default, when a username and password is required, this handler throws
 * up a message box requesting the desired info. However, applications can
 * {@link #setAuthorizationPrompter(HTTPClient.AuthorizationPrompter) set their
 * own authorization prompter} if desired.
 *
 * <P><strong>Note:</strong> all methods except for
 * <var>setAuthorizationPrompter</var> are meant to be invoked by the
 * AuthorizationModule only, i.e. should not be invoked by the application
 * (those methods are only public because implementing the
 * <var>AuthorizationHandler</var> interface requires them to be).
 *
 * @version	0.3-3  06/05/2001
 * @author	Ronald Tschalär
 * @since	V0.2
 */
public class DefaultAuthHandler implements AuthorizationHandler, GlobalConstants
{
    private static final byte[] NUL = new byte[0];

    private static final int DI_A1  = 0;
    private static final int DI_A1S = 1;
    private static final int DI_QOP = 2;

    private static byte[] digest_secret = null;

    private static AuthorizationPrompter prompter    = null;
    private static boolean               prompterSet = false;

    //HS:
    private static Random random = new Random();


    /**
     * For Digest authentication we need to set the uri, response and
     * opaque parameters. For "Basic" and "SOCKS5" nothing is done.
     */
    public AuthorizationInfo fixupAuthInfo(AuthorizationInfo info,
					   RoRequest req,
					   AuthorizationInfo challenge,
					   RoResponse resp)
		    throws AuthSchemeNotImplException
    {
	// nothing to do for Basic and SOCKS5 schemes

	if (info.getScheme().equalsIgnoreCase("Basic")  ||
	    info.getScheme().equalsIgnoreCase("SOCKS5"))
	    return info;
	else if (!info.getScheme().equalsIgnoreCase("Digest"))
	    throw new AuthSchemeNotImplException(info.getScheme());

	if (Log.isEnabled(Log.AUTH))
	    Log.write(Log.AUTH, "Auth:  fixing up Authorization for host " +
				info.getHost()+":"+info.getPort() +
				"; scheme: " + info.getScheme() +
				"; realm: " + info.getRealm());

	return digest_fixup(info, req, challenge, resp);
    }


    /**
     * returns the requested authorization, or null if none was given.
     *
     * @param challenge the parsed challenge from the server.
     * @param req the request which solicited this response
     * @param resp the full response received
     * @return a structure containing the necessary authorization info,
     *         or null
     * @exception AuthSchemeNotImplException if the authentication scheme
     *             in the challenge cannot be handled.
     */
    public AuthorizationInfo getAuthorization(AuthorizationInfo challenge,
					      RoRequest req, RoResponse resp)
		    throws AuthSchemeNotImplException, IOException
    {
	AuthorizationInfo cred;


	if (Log.isEnabled(Log.AUTH))
	    Log.write(Log.AUTH, "Auth:  Requesting Authorization for host " +
				challenge.getHost()+":"+challenge.getPort() +
				"; scheme: " + challenge.getScheme() +
				"; realm: " + challenge.getRealm());


	// we only handle Basic, Digest and SOCKS5 authentication

	if (!challenge.getScheme().equalsIgnoreCase("Basic")  &&
	    !challenge.getScheme().equalsIgnoreCase("Digest")  &&
	    !challenge.getScheme().equalsIgnoreCase("SOCKS5"))
	    throw new AuthSchemeNotImplException(challenge.getScheme());


	// For digest authentication, check if stale is set

	if (challenge.getScheme().equalsIgnoreCase("Digest"))
	{
	    cred = digest_check_stale(challenge, req, resp);
	    if (cred != null)
		return cred;
	}


	// Ask the user for username/password

	NVPair answer;
	synchronized (getClass())
	{
	    if (!req.allowUI()  ||  prompterSet  &&  prompter == null)
		return null;

	    if (prompter == null)
		setDefaultPrompter();

	    answer = prompter.getUsernamePassword(challenge,
						  resp.getStatusCode() == 407);
	}

	if (answer == null)
	    return null;


	// Now process the username/password

	if (challenge.getScheme().equalsIgnoreCase("basic"))
	{
	    cred = new AuthorizationInfo(challenge.getHost(),
					 challenge.getPort(),
					 challenge.getScheme(),
					 challenge.getRealm(),
					 Codecs.base64Encode(
						answer.getName() + ":" +
						answer.getValue()));
	}
	else if (challenge.getScheme().equalsIgnoreCase("Digest"))
	{
	    cred = digest_gen_auth_info(challenge.getHost(),
					challenge.getPort(),
				        challenge.getRealm(), answer.getName(),
					answer.getValue(),
					req.getConnection().getContext());
	    cred = digest_fixup(cred, req, challenge, null);
	}
	else	// SOCKS5
	{
	    NVPair[] upwd = { answer };
	    cred = new AuthorizationInfo(challenge.getHost(),
					 challenge.getPort(),
					 challenge.getScheme(),
					 challenge.getRealm(),
					 upwd, null);
	}


	// try to get rid of any unencoded passwords in memory

	answer = null;
        //HS:
	//System.gc();


	// Done

	Log.write(Log.AUTH, "Auth:  Got Authorization");

	return cred;
    }


    /**
     * We handle the "Authentication-Info" and "Proxy-Authentication-Info"
     * headers here.
     */
    public void handleAuthHeaders(Response resp, RoRequest req,
				  AuthorizationInfo prev,
				  AuthorizationInfo prxy)
	    throws IOException
    {
	String auth_info = resp.getHeader("Authentication-Info");
	String prxy_info = resp.getHeader("Proxy-Authentication-Info");

	if (auth_info == null  &&  prev != null  &&
	    hasParam(prev.getParams(), "qop", "auth-int"))
	    auth_info = "";

	if (prxy_info == null  &&  prxy != null  &&
	    hasParam(prxy.getParams(), "qop", "auth-int"))
	    prxy_info = "";

	try
	{
	    handleAuthInfo(auth_info, "Authentication-Info", prev, resp, req,
			   true);
	    handleAuthInfo(prxy_info, "Proxy-Authentication-Info", prxy, resp,
			   req, true);
	}
	catch (ParseException pe)
	    { throw new IOException(pe.toString()); }
    }


    /**
     * We handle the "Authentication-Info" and "Proxy-Authentication-Info"
     * trailers here.
     */
    public void handleAuthTrailers(Response resp, RoRequest req,
				   AuthorizationInfo prev,
				   AuthorizationInfo prxy)
	    throws IOException
    {
	String auth_info = resp.getTrailer("Authentication-Info");
	String prxy_info = resp.getTrailer("Proxy-Authentication-Info");

	try
	{
	    handleAuthInfo(auth_info, "Authentication-Info", prev, resp, req,
			   false);
	    handleAuthInfo(prxy_info, "Proxy-Authentication-Info", prxy, resp,
			   req, false);
	}
	catch (ParseException pe)
	    { throw new IOException(pe.toString()); }
    }


    private static void handleAuthInfo(String auth_info, String hdr_name,
				       AuthorizationInfo prev, Response resp,
				       RoRequest req, boolean in_headers)
	    throws ParseException, IOException
    {
	if (auth_info == null)  return;

	Vector pai = Util.parseHeader(auth_info);
	HttpHeaderElement elem;

	if (handle_nextnonce(prev, req,
			     elem = Util.getElement(pai, "nextnonce")))
	    pai.removeElement(elem);
	if (handle_discard(prev, req, elem = Util.getElement(pai, "discard")))
	    pai.removeElement(elem);

	if (in_headers)
	{
	    HttpHeaderElement qop = null;

	    if (pai != null  &&
		(qop = Util.getElement(pai, "qop")) != null  &&
		qop.getValue() != null)
	    {
		handle_rspauth(prev, resp, req, pai, hdr_name);
	    }
	    else if (prev != null  &&
		     (Util.hasToken(resp.getHeader("Trailer"), hdr_name)  &&
		      hasParam(prev.getParams(), "qop", null)  ||
		      hasParam(prev.getParams(), "qop", "auth-int")))
	    {
		handle_rspauth(prev, resp, req, null, hdr_name);
	    }

	    else if ((pai != null  &&  qop == null  &&
		      pai.contains(new HttpHeaderElement("digest")))  ||
		     (Util.hasToken(resp.getHeader("Trailer"), hdr_name)  &&
		      prev != null  &&
		      !hasParam(prev.getParams(), "qop", null)))
	    {
		handle_digest(prev, resp, req, hdr_name);
	    }
	}

	if (pai.size() > 0)
	    resp.setHeader(hdr_name, Util.assembleHeader(pai));
	else
	    resp.deleteHeader(hdr_name);
    }


    private static final boolean hasParam(NVPair[] params, String name,
					  String val)
    {
	for (int idx=0; idx<params.length; idx++)
	    if (params[idx].getName().equalsIgnoreCase(name)  &&
		(val == null  ||  params[idx].getValue().equalsIgnoreCase(val)))
		return true;

	return false;
    }


    /*
     * Here are all the Digest specific methods
     */

    private static AuthorizationInfo digest_gen_auth_info(String host, int port,
							  String realm,
							  String user,
							  String pass,
							  Object context)
    {
	String A1 = user + ":" + realm + ":" + pass;
	String[] info = { MD5.hexDigest(A1), null, null };

	AuthorizationInfo prev = AuthorizationInfo.getAuthorization(host, port,
						    "Digest", realm, context);
	NVPair[] params;
	if (prev == null)
	{
	    params = new NVPair[4];
	    params[0] = new NVPair("username", user);
	    params[1] = new NVPair("uri", "");
	    params[2] = new NVPair("nonce", "");
	    params[3] = new NVPair("response", "");
	}
	else
	{
	    params = prev.getParams();
	    for (int idx=0; idx<params.length; idx++)
	    {
		if (params[idx].getName().equalsIgnoreCase("username"))
		{
		    params[idx] = new NVPair("username", user);
		    break;
		}
	    }
	}

	return new AuthorizationInfo(host, port, "Digest", realm, params, info);
    }


    /**
     * The fixup handler
     */
    private static AuthorizationInfo digest_fixup(AuthorizationInfo info,
						  RoRequest req,
						  AuthorizationInfo challenge,
						  RoResponse resp)
	    throws AuthSchemeNotImplException
    {
	// get various parameters from challenge

	int ch_domain=-1, ch_nonce=-1, ch_alg=-1, ch_opaque=-1, ch_stale=-1,
	    ch_dreq=-1, ch_qop=-1;
	NVPair[] ch_params = null;
	if (challenge != null)
	{
	    ch_params = challenge.getParams();

	    for (int idx=0; idx<ch_params.length; idx++)
	    {
		String name = ch_params[idx].getName().toLowerCase();
		if (name.equals("domain"))               ch_domain = idx;
		else if (name.equals("nonce"))           ch_nonce  = idx;
		else if (name.equals("opaque"))          ch_opaque = idx;
		else if (name.equals("algorithm"))       ch_alg    = idx;
		else if (name.equals("stale"))           ch_stale  = idx;
		else if (name.equals("digest-required")) ch_dreq   = idx;
		else if (name.equals("qop"))             ch_qop    = idx;
	    }
	}


	// get various parameters from info

	int uri=-1, user=-1, alg=-1, response=-1, nonce=-1, cnonce=-1, nc=-1,
	    opaque=-1, digest=-1, dreq=-1, qop=-1;
	NVPair[] params;
	String[] extra;

	synchronized (info)	// we need to juggle nonce, nc, etc
	{
	    params = info.getParams();

	    for (int idx=0; idx<params.length; idx++)
	    {
		String name = params[idx].getName().toLowerCase();
		if (name.equals("uri"))                  uri      = idx;
		else if (name.equals("username"))        user     = idx;
		else if (name.equals("algorithm"))       alg      = idx;
		else if (name.equals("nonce"))           nonce    = idx;
		else if (name.equals("cnonce"))          cnonce   = idx;
		else if (name.equals("nc"))              nc       = idx;
		else if (name.equals("response"))        response = idx;
		else if (name.equals("opaque"))          opaque   = idx;
		else if (name.equals("digest"))          digest   = idx;
		else if (name.equals("digest-required")) dreq     = idx;
		else if (name.equals("qop"))             qop      = idx;
	    }

	    extra = (String[]) info.getExtraInfo();


	    // currently only MD5 hash (and "MD5-sess") is supported

	    if (alg != -1  &&
		!params[alg].getValue().equalsIgnoreCase("MD5")  &&
		!params[alg].getValue().equalsIgnoreCase("MD5-sess"))
		throw new AuthSchemeNotImplException("Digest auth scheme: " +
				    "Algorithm " + params[alg].getValue() +
				    " not implemented");

	    if (ch_alg != -1  &&
		!ch_params[ch_alg].getValue().equalsIgnoreCase("MD5") &&
		!ch_params[ch_alg].getValue().equalsIgnoreCase("MD5-sess"))
		throw new AuthSchemeNotImplException("Digest auth scheme: " +
				    "Algorithm " + ch_params[ch_alg].getValue()+
				    " not implemented");


	    // fix up uri and nonce

	    params[uri] = new NVPair("uri",
		    URI.escape(req.getRequestURI(), URI.escpdPathChar, false));
	    String old_nonce = params[nonce].getValue();
	    if (ch_nonce != -1  &&
		!old_nonce.equals(ch_params[ch_nonce].getValue()))
		params[nonce] = ch_params[ch_nonce];


	    // update or add optional attributes (opaque, algorithm, cnonce,
	    // nonce-count, and qop

	    if (ch_opaque != -1)
	    {
		if (opaque == -1)
		{
		    params = Util.resizeArray(params, params.length+1);
		    opaque = params.length-1;
		}
		params[opaque] = ch_params[ch_opaque];
	    }

	    if (ch_alg != -1)
	    {
		if (alg == -1)
		{
		    params = Util.resizeArray(params, params.length+1);
		    alg = params.length-1;
		}
		params[alg] = ch_params[ch_alg];
	    }

	    if (ch_qop != -1  ||
		(ch_alg != -1  &&
		 ch_params[ch_alg].getValue().equalsIgnoreCase("MD5-sess")))
	    {
		if (cnonce == -1)
		{
		    params = Util.resizeArray(params, params.length+1);
		    cnonce = params.length-1;
		}

		if (digest_secret == null)
		    digest_secret = gen_random_bytes(20);

		long l_time = System.currentTimeMillis();
		byte[] time = new byte[8];
		time[0] = (byte) (l_time & 0xFF);
		time[1] = (byte) ((l_time >>  8) & 0xFF);
		time[2] = (byte) ((l_time >> 16) & 0xFF);
		time[3] = (byte) ((l_time >> 24) & 0xFF);
		time[4] = (byte) ((l_time >> 32) & 0xFF);
		time[5] = (byte) ((l_time >> 40) & 0xFF);
		time[6] = (byte) ((l_time >> 48) & 0xFF);
		time[7] = (byte) ((l_time >> 56) & 0xFF);

		params[cnonce] =
		    new NVPair("cnonce", MD5.hexDigest(digest_secret, time));
	    }


	    // select qop option

	    if (ch_qop != -1)
	    {
		if (qop == -1)
		{
		    params = Util.resizeArray(params, params.length+1);
		    qop = params.length-1;
		}
		extra[DI_QOP] = ch_params[ch_qop].getValue();


		// select qop option

		String[] qops = splitList(extra[DI_QOP], ",");
		String p = null;
		for (int idx=0; idx<qops.length; idx++)
		{
		    if (qops[idx].equalsIgnoreCase("auth-int")  &&
			(req.getStream() == null  ||
			 req.getConnection().ServProtVersKnown  &&
			 req.getConnection().ServerProtocolVersion >= HTTP_1_1))
		    {
			p = "auth-int";
			break;
		    }
		    if (qops[idx].equalsIgnoreCase("auth"))
			p = "auth";
		}
		if (p == null)
		{
		    for (int idx=0; idx<qops.length; idx++)
			if (qops[idx].equalsIgnoreCase("auth-int"))
			    throw new AuthSchemeNotImplException(
				"Digest auth scheme: Can't comply with qop " +
				"option 'auth-int' because an HttpOutputStream " +
				"is being used and the server doesn't speak " +
				"HTTP/1.1");

		    throw new AuthSchemeNotImplException("Digest auth scheme: "+
				"None of the available qop options '" +
				ch_params[ch_qop].getValue() + "' implemented");
		}
		params[qop] = new NVPair("qop", p);
	    }


	    // increment nonce-count.

	    if (qop != -1)
	    {
		/* Note: we should actually be serializing all requests through
		 *       here so that the server sees the nonce-count in a
		 *       strictly increasing order. However, this would be a
		 *       *major* hassle to do, so we're just winging it. Most
		 *       of the time the requests will go over the wire in the
		 *       same order as they pass through here, but in MT apps
		 *       it's possible for one request to "overtake" another
		 *       between here and the synchronized block in
		 *       sendRequest().
		 */
		if (nc == -1)
		{
		    params = Util.resizeArray(params, params.length+1);
		    nc = params.length-1;
		    params[nc] = new NVPair("nc", "00000001");
		}
		else if (old_nonce.equals(params[nonce].getValue()))
		{
		    String c = Long.toHexString(
				Long.parseLong(params[nc].getValue(), 16) + 1);
		    params[nc] =
			new NVPair("nc", "00000000".substring(c.length()) + c);
		}
		else
		    params[nc] = new NVPair("nc", "00000001");
	    }


	    // calc new session key if necessary

	    if (challenge != null  &&
		(ch_stale == -1  ||
		 !ch_params[ch_stale].getValue().equalsIgnoreCase("true"))  &&
		alg != -1  &&
		params[alg].getValue().equalsIgnoreCase("MD5-sess"))
	    {
		extra[DI_A1S] = MD5.hexDigest(extra[DI_A1] + ":" +
					      params[nonce].getValue() + ":" +
					      params[cnonce].getValue());
	    }


	    // update parameters for next auth cycle

	    info.setParams(params);
	    info.setExtraInfo(extra);
	}


	// calc "response" attribute

	String hash = null;
	if (qop != -1 && params[qop].getValue().equalsIgnoreCase("auth-int"))
	{
	    if (req.getStream() != null)
		throw new AuthSchemeNotImplException(
		    "Digest auth scheme: Can't comply with qop " +
		    "option 'auth-int' because an HttpOutputStream " +
		    "is being used.");
	    hash = MD5.hexDigest(req.getData() == null ? NUL : req.getData());
	}

	params[response] = new NVPair("response", 
	      calcResponseAttr(hash, extra, params, alg, uri, qop, nonce,
			       nc, cnonce, req.getMethod()));


	// calc digest if necessary

	AuthorizationInfo new_info;

	boolean ch_dreq_val = false;
	if (ch_dreq != -1  &&
	    (ch_params[ch_dreq].getValue() == null  ||
	     ch_params[ch_dreq].getValue().equalsIgnoreCase("true")))
	    ch_dreq_val = true;

	if ((ch_dreq_val  ||  digest != -1)  &&  req.getStream() == null)
	{
	    NVPair[] d_params;
	    if (digest == -1)
	    {
		d_params = Util.resizeArray(params, params.length+1);
		digest = params.length;
	    }
	    else
		d_params = params;
	    d_params[digest] = new NVPair("digest",
		   calc_digest(req, extra[DI_A1], params[nonce].getValue()));

	    if (dreq == -1)	// if server requires digest, then so do we...
	    {
		dreq = d_params.length;
		d_params = Util.resizeArray(d_params, d_params.length+1);
		d_params[dreq] = new NVPair("digest-required", "true");
	    }

	    new_info = new AuthorizationInfo(info.getHost(), info.getPort(),
					     info.getScheme(), info.getRealm(),
					     d_params, extra);
	}
	else if (ch_dreq_val)
	    new_info = null;
	else
	    new_info = new AuthorizationInfo(info.getHost(), info.getPort(),
					     info.getScheme(), info.getRealm(),
					     params, extra);


	// add info for other domains, if listed

	boolean from_server = (challenge != null)  &&
	    challenge.getHost().equalsIgnoreCase(req.getConnection().getHost());
	if (ch_domain != -1)
	{
	    URI base = null;
	    try
	    {
		base = new URI(req.getConnection().getProtocol(),
			       req.getConnection().getHost(),
			       req.getConnection().getPort(),
			       req.getRequestURI());
	    }
	    catch (ParseException pe)
		{ }

	    StringTokenizer tok =
			new StringTokenizer(ch_params[ch_domain].getValue());
	    while (tok.hasMoreTokens())
	    {
		URI Uri;
		try
		    { Uri = new URI(base, tok.nextToken()); }
		catch (ParseException pe)
		    { continue; }
		if (Uri.getHost() == null)
		    continue;

		AuthorizationInfo tmp =
		    AuthorizationInfo.getAuthorization(Uri.getHost(),
						       Uri.getPort(),
						       info.getScheme(),
						       info.getRealm(),
					     req.getConnection().getContext());
		if (tmp == null)
		{
		    params[uri] = new NVPair("uri", Uri.getPathAndQuery());
		    tmp = new AuthorizationInfo(Uri.getHost(), Uri.getPort(),
					        info.getScheme(),
						info.getRealm(), params,
						extra);
		    AuthorizationInfo.addAuthorization(tmp);
		}
		if (from_server)
		    tmp.addPath(Uri.getPathAndQuery());
	    }
	}
	else if (from_server  &&  challenge != null)
	{
	    // Spec says that if no domain attribute is present then the
	    // whole server should be considered being in the same space
	    AuthorizationInfo tmp =
		AuthorizationInfo.getAuthorization(challenge.getHost(),
						   challenge.getPort(),
						   info.getScheme(),
						   info.getRealm(),
					     req.getConnection().getContext());
	    if (tmp != null)  tmp.addPath("/");
	}


	// now return the one to use

	return new_info;
    }


    /**
     * @return the fixed info is stale=true; null otherwise
     */
    private static AuthorizationInfo digest_check_stale(
					      AuthorizationInfo challenge,
					      RoRequest req, RoResponse resp)
	    throws AuthSchemeNotImplException, IOException
    {
	AuthorizationInfo cred = null;

	NVPair[] params = challenge.getParams();
	for (int idx=0; idx<params.length; idx++)
	{
	    String name = params[idx].getName();
	    if (name.equalsIgnoreCase("stale")  &&
		params[idx].getValue().equalsIgnoreCase("true"))
	    {
		cred = AuthorizationInfo.getAuthorization(challenge, req, resp,
							  false);
		if (cred != null)	// should always be the case
		    return digest_fixup(cred, req, challenge, resp);
		break;			// should never be reached
	    }
	}

	return cred;
    }


    /**
     * Handle nextnonce field.
     */
    private static boolean handle_nextnonce(AuthorizationInfo prev,
					    RoRequest req,
					    HttpHeaderElement nextnonce)
	    throws IOException
    {
	if (prev == null  ||  nextnonce == null  ||
	    nextnonce.getValue() == null)
	    return false;

	AuthorizationInfo ai;
	try
	    { ai = AuthorizationInfo.getAuthorization(prev, req, null, false); }
	catch (AuthSchemeNotImplException asnie)
	    { ai = prev; /* shouldn't happen */ }
	synchronized (ai)
	{
	    NVPair[] params = ai.getParams();
	    params = setValue(params, "nonce", nextnonce.getValue());
	    params = setValue(params, "nc", "00000000");
	    ai.setParams(params);
	}

	return true;
    }


    /**
     * Handle digest field of the Authentication-Info response header.
     */
    private static boolean handle_digest(AuthorizationInfo prev, Response resp,
					 RoRequest req, String hdr_name)
	    throws IOException
    {
	if (prev == null)
	    return false;

	NVPair[] params = prev.getParams();
	VerifyDigest
	    verifier = new VerifyDigest(((String[]) prev.getExtraInfo())[0],
					getValue(params, "nonce"),
					req.getMethod(),
					getValue(params, "uri"),
					hdr_name, resp);

	if (resp.hasEntity())
	{
	    Log.write(Log.AUTH, "Auth:  pushing md5-check-stream to verify "+
				"digest from " + hdr_name);
	    resp.inp_stream = new MD5InputStream(resp.inp_stream, verifier);
	}
	else
	{
	    Log.write(Log.AUTH, "Auth:  verifying digest from " + hdr_name);
	    verifier.verifyHash(MD5.digest(NUL), 0);
	}

	return true;
    }


    /**
     * Handle rspauth field of the Authentication-Info response header.
     */
    private static boolean handle_rspauth(AuthorizationInfo prev, Response resp,
					  RoRequest req, Vector auth_info,
					  String hdr_name)
	    throws IOException
    {
	if (prev == null)
	    return false;


	// get the parameters we sent

	NVPair[] params = prev.getParams();
	int uri=-1, alg=-1, nonce=-1, cnonce=-1, nc=-1;
	for (int idx=0; idx<params.length; idx++)
	{
	    String name = params[idx].getName().toLowerCase();
	    if (name.equals("uri"))            uri    = idx;
	    else if (name.equals("algorithm")) alg    = idx;
	    else if (name.equals("nonce"))     nonce  = idx;
	    else if (name.equals("cnonce"))    cnonce = idx;
	    else if (name.equals("nc"))        nc     = idx;
	}


	// create hash verifier to verify rspauth

	VerifyRspAuth
	    verifier = new VerifyRspAuth(params[uri].getValue(),
			      ((String[]) prev.getExtraInfo())[0],
			      (alg == -1 ? null : params[alg].getValue()),
						  params[nonce].getValue(),
			      (cnonce == -1 ? "" : params[cnonce].getValue()),
			      (nc == -1 ? "" : params[nc].getValue()),
			      hdr_name, resp);


	// if Authentication-Info in header and qop=auth then verify immediately

	HttpHeaderElement qop = null;
	if (auth_info != null  &&
	    (qop = Util.getElement(auth_info, "qop")) != null  &&
	    qop.getValue() != null  &&
	    (qop.getValue().equalsIgnoreCase("auth")  ||
	     !resp.hasEntity()  &&  qop.getValue().equalsIgnoreCase("auth-int"))
	   )
	{
	    Log.write(Log.AUTH, "Auth:  verifying rspauth from " + hdr_name);
	    verifier.verifyHash(MD5.digest(NUL), 0);
	}
	else
	{
	    // else push md5 stream and verify after body

	    Log.write(Log.AUTH, "Auth:  pushing md5-check-stream to verify "+
				"rspauth from " + hdr_name);
	    resp.inp_stream = new MD5InputStream(resp.inp_stream, verifier);
	}

	return true;
    }


    /**
     * Calc "response" attribute for a request.
     */
    private static String calcResponseAttr(String hash, String[] extra,
					   NVPair[] params, int alg,
					   int uri, int qop, int nonce,
					   int nc, int cnonce, String method)
    {
	String A1, A2, resp_val;

	if (alg != -1  &&
	    params[alg].getValue().equalsIgnoreCase("MD5-sess"))
	    A1 = extra[DI_A1S];
	else
	    A1 = extra[DI_A1];

	A2 = method + ":" + params[uri].getValue();
	if (qop != -1  &&
	    params[qop].getValue().equalsIgnoreCase("auth-int"))
	{
	    A2 += ":" + hash;
	}
	A2 = MD5.hexDigest(A2);

	if (qop == -1)
	    resp_val =
		MD5.hexDigest(A1 + ":" + params[nonce].getValue() + ":" + A2);
	else
	    resp_val =
		MD5.hexDigest(A1 + ":" + params[nonce].getValue() + ":" +
			      params[nc].getValue() + ":" +
			      params[cnonce].getValue() + ":" +
			      params[qop].getValue() + ":" + A2);

	return resp_val;
    }


    /**
     * Calculates the digest of the request body. This was in RFC-2069
     * and draft-ietf-http-authentication-00.txt, but has subsequently
     * been removed. Here for backwards compatibility.
     */
    private static String calc_digest(RoRequest req, String A1_hash,
				      String nonce)
    {
	if (req.getStream() != null)
	    return "";

	int ct=-1, ce=-1, lm=-1, ex=-1, dt=-1;
	for (int idx=0; idx<req.getHeaders().length; idx++)
	{
	    String name = req.getHeaders()[idx].getName();
	    if (name.equalsIgnoreCase("Content-type"))
		ct = idx;
	    else if (name.equalsIgnoreCase("Content-Encoding"))
		ce = idx;
	    else if (name.equalsIgnoreCase("Last-Modified"))
		lm = idx;
	    else if (name.equalsIgnoreCase("Expires"))
		ex = idx;
	    else if (name.equalsIgnoreCase("Date"))
		dt = idx;
	}


	NVPair[] hdrs = req.getHeaders();
	byte[] entity_body = (req.getData() == null ? NUL : req.getData());
	String entity_hash = MD5.hexDigest(entity_body);

	String entity_info = MD5.hexDigest(req.getRequestURI() + ":" +
	     (ct == -1 ? "" : hdrs[ct].getValue()) + ":" +
	     entity_body.length + ":" +
	     (ce == -1 ? "" : hdrs[ce].getValue()) + ":" +
	     (lm == -1 ? "" : hdrs[lm].getValue()) + ":" +
	     (ex == -1 ? "" : hdrs[ex].getValue()));
	String entity_digest = A1_hash + ":" + nonce + ":" + req.getMethod() +
			":" + (dt == -1 ? "" : hdrs[dt].getValue()) +
			":" + entity_info + ":" + entity_hash;

	if (Log.isEnabled(Log.AUTH))
	{
	    Log.write(Log.AUTH, "Auth:  Entity-Info: '" + req.getRequestURI() + ":" +
		 (ct == -1 ? "" : hdrs[ct].getValue()) + ":" +
		 entity_body.length + ":" +
		 (ce == -1 ? "" : hdrs[ce].getValue()) + ":" +
		 (lm == -1 ? "" : hdrs[lm].getValue()) + ":" +
		 (ex == -1 ? "" : hdrs[ex].getValue()) +"'");
	    Log.write(Log.AUTH, "Auth:  Entity-Body: '" + entity_hash + "'");
	    Log.write(Log.AUTH, "Auth:  Entity-Digest: '" + entity_digest + "'");
	}

	return MD5.hexDigest(entity_digest);
    }


    /**
     * Handle discard token
     */
    private static boolean handle_discard(AuthorizationInfo prev, RoRequest req,
					  HttpHeaderElement discard)
    {
	if (discard != null  &&  prev != null)
	{
	    AuthorizationInfo.removeAuthorization(prev,
					    req.getConnection().getContext());
	    return true;
	}

	return false;
    }


    /**
     * Generate <var>num</var> bytes of random data.
     *
     * @param num  the number of bytes to generate
     * @return a byte array of random data
     */
    /*HS: ********************************************************************
    private static byte[] gen_random_bytes(int num)
    {
	// first try /dev/random
	try
	{
	    FileInputStream rnd = new FileInputStream("/dev/random");
	    DataInputStream din = new DataInputStream(rnd);
	    byte[] data = new byte[num];
	    din.readFully(data);
	    try { din.close(); } catch (IOException ioe) { }
	    return data;
	}
	catch (Throwable t)
	    { }

	// This is probably a much better generator, but it can be awfully
	// slow (~ 6 secs / byte on my old LX)
	//return new java.security.SecureRandom().getSeed(num);

	// This is faster, but needs to be done better... 
	byte[] data = new byte[num];
	try
	{
	    long fm = Runtime.getRuntime().freeMemory();
	    data[0] = (byte) (fm & 0xFF);
	    data[1] = (byte) ((fm >>  8) & 0xFF);

	    int h = data.hashCode();
	    data[2] = (byte) (h & 0xFF);
	    data[3] = (byte) ((h >>  8) & 0xFF);
	    data[4] = (byte) ((h >> 16) & 0xFF);
	    data[5] = (byte) ((h >> 24) & 0xFF);

	    long time = System.currentTimeMillis();
	    data[6] = (byte) (time & 0xFF);
	    data[7] = (byte) ((time >>  8) & 0xFF);
	}
	catch (ArrayIndexOutOfBoundsException aioobe)
	    { }

	return data;
    }
    *************************************************************************/

    private static byte[] gen_random_bytes(int num)
    {
        // Reading the contents of /dev/random may be incredibly slow on some
        // Linux systems.

	byte[] data = new byte[num];
        synchronized (random) {
            random.nextBytes(data);
        }
        return data;
    }

    /**
     * Return the value of the first NVPair whose name matches the key
     * using a case-insensitive search.
     *
     * @param list an array of NVPair's
     * @param key  the key to search for
     * @return the value of the NVPair with that key, or null if not
     *         found.
     */
    private final static String getValue(NVPair[] list, String key)
    {
	int len = list.length;

	for (int idx=0; idx<len; idx++)
	    if (list[idx].getName().equalsIgnoreCase(key))
		return list[idx].getValue();

	return null;
    }

    /**
     * Return the index of the first NVPair whose name matches the key
     * using a case-insensitive search.
     *
     * @param list an array of NVPair's
     * @param key  the key to search for
     * @return the index of the NVPair with that key, or -1 if not
     *         found.
     */
    private final static int getIndex(NVPair[] list, String key)
    {
	int len = list.length;

	for (int idx=0; idx<len; idx++)
	    if (list[idx].getName().equalsIgnoreCase(key))
		return idx;

	return -1;
    }

    /**
     * Sets the value of the NVPair with the name that matches the key
     * (case-insensitive). If no name matches, a new entry is created.
     *
     * @param list an array of NVPair's
     * @param key  the name of the NVPair
     * @param val  the value of the new NVPair
     * @return the (possibly) new list
     */
    private final static NVPair[] setValue(NVPair[] list, String key, String val)
    {
	int idx = getIndex(list, key);
	if (idx == -1)
	{
	    idx = list.length;
	    list = Util.resizeArray(list, list.length+1);
	}

	list[idx] = new NVPair(key, val);
	return list;
    }


    /**
     * Split a list into an array of Strings, using sep as the
     * separator and removing whitespace around the separator.
     */
    private static String[] splitList(String str, String sep)
    {
	if (str == null)  return new String[0];

	StringTokenizer tok = new StringTokenizer(str, sep);
	String[] list = new String[tok.countTokens()];
	for (int idx=0; idx<list.length; idx++)
	    list[idx] = tok.nextToken().trim();

	return list;
    }


    /**
     * Produce a string of the form "A5:22:F1:0B:53"
     */
    static String hex(byte[] buf)
    {
	StringBuffer str = new StringBuffer(buf.length*3);
	for (int idx=0; idx<buf.length; idx++)
	{
	    str.append(Character.forDigit((buf[idx] >> 4) & 15, 16));
	    str.append(Character.forDigit(buf[idx] & 15, 16));
	    str.append(':');
	}
	str.setLength(str.length()-1);

	return str.toString();
    }


    static final byte[] unHex(String hex)
    {
	byte[] digest = new byte[hex.length()/2];

	for (int idx=0; idx<digest.length; idx++)
	{
	    digest[idx] = (byte) (0xFF & Integer.parseInt(
				  hex.substring(2*idx, 2*(idx+1)), 16));
	}

	return digest;
    }


    /**
     * Set a new username/password prompter.
     *
     * @param prompt the AuthorizationPrompter to use whenever a username
     *               and password are needed; if null, no querying will be
     *               done
     * @return the previous prompter
     */
    public static synchronized AuthorizationPrompter setAuthorizationPrompter(
					    AuthorizationPrompter prompt)
    {
	AuthorizationPrompter prev = prompter;
	prompter    = prompt;
	prompterSet = true;
	return prev;
    }


    /**
     * Set the default authorization prompter. It first tries to figure out
     * if the AWT is running, and if it is then the GUI popup prompter is used;
     * otherwise the command line prompter is used.
     */
    private static void setDefaultPrompter()
    {
	// if the AWT is running use the popup box; else use the
	// the command line prompter.
	if (!SimpleAuthPrompt.canUseCLPrompt()  ||  isAWTRunning())
	    prompter = new SimpleAuthPopup();
	else
	    prompter = new SimpleAuthPrompt();
    }

    /**
     * Try and figure out if the AWT is running. This is done by searching all
     * threads and looking for one whose name starts with "AWT-". 
     */
    private static final boolean isAWTRunning() {
	// find top-level thread group
	ThreadGroup root = Thread.currentThread().getThreadGroup();
	while (root.getParent() != null)
	    root = root.getParent();

	// search all threads
	Thread[] t_list = new Thread[root.activeCount() + 5];
	int t_num = root.enumerate(t_list);
	for (int idx=0; idx<t_num; idx++)
	{
	    if (t_list[idx].getName().startsWith("AWT-"))
		return true;
	}

	return false;
    }
}


/**
 * This verifies the "rspauth" from draft-ietf-http-authentication-03
 */
class VerifyRspAuth implements HashVerifier, GlobalConstants
{
    private String     uri;
    private String     HA1;
    private String     alg;
    private String     nonce;
    private String     cnonce;
    private String     nc;
    private String     hdr;
    private RoResponse resp;


    public VerifyRspAuth(String uri, String HA1, String alg, String nonce,
			 String cnonce, String nc, String hdr, RoResponse resp)
    {
	this.uri    = uri;
	this.HA1    = HA1;
	this.alg    = alg;
	this.nonce  = nonce;
	this.cnonce = cnonce;
	this.nc     = nc;
	this.hdr    = hdr;
	this.resp   = resp;
    }


    public void verifyHash(byte[] hash, long len)  throws IOException
    {
	String auth_info = resp.getHeader(hdr);
	if (auth_info == null)
	    auth_info = resp.getTrailer(hdr);
	if (auth_info == null)
	    return;

	Vector pai;
	try
	    { pai = Util.parseHeader(auth_info); }
	catch (ParseException pe)
	    { throw new IOException(pe.toString()); }

	String qop;
	HttpHeaderElement elem = Util.getElement(pai, "qop");
	if (elem == null  ||  (qop = elem.getValue()) == null  ||
	    (!qop.equalsIgnoreCase("auth")  &&
	     !qop.equalsIgnoreCase("auth-int")))
	    return;

	elem = Util.getElement(pai, "rspauth");
	if (elem == null  ||  elem.getValue() == null) return;
	byte[] digest = DefaultAuthHandler.unHex(elem.getValue());

	elem = Util.getElement(pai, "cnonce");
	if (elem != null  &&  elem.getValue() != null  &&
	    !elem.getValue().equals(cnonce))
	    throw new IOException("Digest auth scheme: received wrong " +
				  "client-nonce '" + elem.getValue() +
				  "' - expected '" + cnonce + "'");

	elem = Util.getElement(pai, "nc");
	if (elem != null  &&  elem.getValue() != null  &&
	    !elem.getValue().equals(nc))
	    throw new IOException("Digest auth scheme: received wrong " +
				  "nonce-count '" + elem.getValue() +
				  "' - expected '" + nc + "'");

	String A1, A2;
	if (alg != null  &&  alg.equalsIgnoreCase("MD5-sess"))
	    A1 = MD5.hexDigest(HA1 + ":" + nonce + ":" + cnonce);
	else
	    A1 = HA1;

	// draft-01 was: A2 = resp.getStatusCode() + ":" + uri;
	A2 = ":" + uri;
	if (qop.equalsIgnoreCase("auth-int"))
	    A2 += ":" + MD5.toHex(hash);
	A2 = MD5.hexDigest(A2);

	hash = MD5.digest(A1 + ":" + nonce + ":" +  nc + ":" + cnonce + ":" +
			  qop + ":" + A2);

	for (int idx=0; idx<hash.length; idx++)
	{
	    if (hash[idx] != digest[idx])
		throw new IOException("MD5-Digest mismatch: expected " +
				      DefaultAuthHandler.hex(digest) +
				      " but calculated " +
				      DefaultAuthHandler.hex(hash));
	}

	Log.write(Log.AUTH, "Auth:  rspauth from " + hdr +
			    " successfully verified");
    }
}


/**
 * This verifies the "digest" from rfc-2069
 */
class VerifyDigest implements HashVerifier, GlobalConstants
{
    private String     HA1;
    private String     nonce;
    private String     method;
    private String     uri;
    private String     hdr;
    private RoResponse resp;


    public VerifyDigest(String HA1, String nonce, String method, String uri,
			String hdr, RoResponse resp)
    {
	this.HA1    = HA1;
	this.nonce  = nonce;
	this.method = method;
	this.uri    = uri;
	this.hdr    = hdr;
	this.resp   = resp;
    }


    public void verifyHash(byte[] hash, long len)  throws IOException
    {
	String auth_info = resp.getHeader(hdr);
	if (auth_info == null)
	    auth_info = resp.getTrailer(hdr);
	if (auth_info == null)
	    return;

	Vector pai;
	try
	    { pai = Util.parseHeader(auth_info); }
	catch (ParseException pe)
	    { throw new IOException(pe.toString()); }
	HttpHeaderElement elem = Util.getElement(pai, "digest");
	if (elem == null  ||  elem.getValue() == null)
	    return;

	byte[] digest = DefaultAuthHandler.unHex(elem.getValue());

	String entity_info = MD5.hexDigest(
				uri + ":" +
				header_val("Content-Type", resp) + ":" +
				header_val("Content-Length", resp) + ":" +
				header_val("Content-Encoding", resp) + ":" +
				header_val("Last-Modified", resp) + ":" +
				header_val("Expires", resp));
	hash = MD5.digest(HA1 + ":" + nonce + ":" + method + ":" +
			  header_val("Date", resp) +
			  ":" + entity_info + ":" + MD5.toHex(hash));

	for (int idx=0; idx<hash.length; idx++)
	{
	    if (hash[idx] != digest[idx])
		throw new IOException("MD5-Digest mismatch: expected " +
				      DefaultAuthHandler.hex(digest) +
				      " but calculated " +
				      DefaultAuthHandler.hex(hash));
	}

	Log.write(Log.AUTH, "Auth:  digest from " + hdr +
			    " successfully verified");
    }


    private static final String header_val(String hdr_name, RoResponse resp)
	    throws IOException
    {
	String hdr = resp.getHeader(hdr_name);
	String tlr = resp.getTrailer(hdr_name);
	return (hdr != null ? hdr : (tlr != null ? tlr : ""));
    }
}


class SimpleAuthPopup implements AuthorizationPrompter
{
    private static BasicAuthBox inp = null;

    /**
     * the method called by DefaultAuthHandler.
     *
     * @return the username/password pair
     */
    public NVPair getUsernamePassword(AuthorizationInfo challenge, boolean forProxy)
    {
	String line1, line2, line3;

	if (challenge.getScheme().equalsIgnoreCase("SOCKS5"))
	{
	    line1 = "Enter username and password for SOCKS server on host";
	    line2 = challenge.getHost();
	    line3 = "Authentication Method: username/password";
	}
	else
	{
	    line1 = "Enter username and password for realm `" +
		    challenge.getRealm() + "'";
	    line2 = "on host " + challenge.getHost() + ":" +
		    challenge.getPort();
	    line3 = "Authentication Scheme: " + challenge.getScheme();
	}

	synchronized(getClass())
	{
	    if (inp == null)
		inp = new BasicAuthBox();
	}

	return inp.getInput(line1, line2, line3, challenge.getScheme());
    }


    /**
     * This class implements a simple popup that request username and password
     * used for the "basic" and "digest" authentication schemes.
     *
     * @version	0.3-3  06/05/2001
     * @author	Ronald Tschalär
     */
    private static class BasicAuthBox extends Frame
    {
	private final static String title = "Authorization Request";
	private Dimension           screen;
	private Label               line1, line2, line3;
	private TextField           user, pass;
	private int                 done;
	private final static int    OK = 1, CANCEL = 0;


	/**
	 * Constructs the popup with two lines of text above the input fields
	 */
	BasicAuthBox()
	{
	    super(title);

	    screen = getToolkit().getScreenSize();

	    addNotify();
	    addWindowListener(new Close());
	    setLayout(new BorderLayout());

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

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

	    GridBagLayout gb = new GridBagLayout();
	    p = new Panel(gb);
	    GridBagConstraints constr = new GridBagConstraints();
	    Panel pp = new Panel();
	    p.add(pp);
	    constr.gridwidth = GridBagConstraints.REMAINDER;
	    gb.setConstraints(pp, constr);
	    constr.gridwidth = 1;
	    constr.weightx = 1.0;
	    Button b;
	    p.add(b = new Button("  OK  "));
	    b.addActionListener(new Ok());
	    constr.weightx = 1.0;
	    gb.setConstraints(b, constr);
	    p.add(b = new Button("Clear"));
	    b.addActionListener(new Clear());
	    constr.weightx = 2.0;
	    gb.setConstraints(b, constr);
	    p.add(b = new Button("Cancel"));
	    b.addActionListener(new Cancel());
	    constr.weightx = 1.0;
	    gb.setConstraints(b, constr);
	    add("South", p);

	    pack();
	}


	/**
	 * our event handlers
	 */
	private class Ok implements ActionListener
	{
	    public void actionPerformed(ActionEvent ae)
	    {
		done = OK;
		synchronized (BasicAuthBox.this)
		    { BasicAuthBox.this.notifyAll(); }
	    }
	}

	private class Clear implements ActionListener
	{
	    public void actionPerformed(ActionEvent ae)
	    {
		user.setText("");
		pass.setText("");
		user.requestFocus();
	    }
	}

	private class Cancel implements ActionListener
	{
	    public void actionPerformed(ActionEvent ae)
	    {
		done = CANCEL;
		synchronized (BasicAuthBox.this)
		    { BasicAuthBox.this.notifyAll(); }
	    }
	}


	private class Close extends WindowAdapter
	{
	    public void windowClosing(WindowEvent we)
	    {
		new Cancel().actionPerformed(null);
	    }
	}


	/**
	 * the method called by SimpleAuthPopup.
	 *
	 * @return the username/password pair
	 */
	synchronized NVPair getInput(String l1, String l2, String l3,
				     String scheme)
	{
	    line1.setText(l1);
	    line2.setText(l2);
	    line3.setText(l3);

	    line1.invalidate();
	    line2.invalidate();
	    line3.invalidate();

	    setResizable(true);
	    pack();
	    setResizable(false);
	    setLocation((screen.width-getPreferredSize().width)/2,
			(int) ((screen.height-getPreferredSize().height)/2*.7));

	    boolean user_focus = true;
	    if (scheme.equalsIgnoreCase("NTLM"))
	    {
		// prefill the user field with the username
		try
		{
		    user.setText(System.getProperty("user.name", ""));
		    user_focus = false;
		}
		catch (SecurityException se)
		    { }
	    }

	    setVisible(true);
	    if (user_focus)
		user.requestFocus();
	    else
		pass.requestFocus();

	    try { wait(); } catch (InterruptedException e) { }

	    setVisible(false);

	    NVPair result = new NVPair(user.getText(), pass.getText());
	    user.setText("");
	    pass.setText("");

	    if (done == CANCEL)
		return null;
	    else
		return result;
	}
    }
}


/**
 * This class implements a simple command line prompter that request
 * username and password used for the "basic" and "digest" authentication
 * schemes.
 *
 * @version	0.3-3  06/05/2001
 * @author	Ronald Tschalär
 */
class SimpleAuthPrompt implements AuthorizationPrompter
{
    /**
     * the method called by DefaultAuthHandler.
     *
     * @return the username/password pair
     */
    public NVPair getUsernamePassword(AuthorizationInfo challenge, boolean forProxy)
    {
	String user, pass;

	if (challenge.getScheme().equalsIgnoreCase("SOCKS5"))
	{
	    System.out.println("Enter username and password for SOCKS " +
			       "server on host " + challenge.getHost());
	    System.out.println("Authentication Method: username/password");
	}
	else
	{
	    System.out.println("Enter username and password for realm `" +
			       challenge.getRealm() + "' on host " +
			       challenge.getHost() + ":" +
			       challenge.getPort());
	    System.out.println("Authentication Scheme: " +
			       challenge.getScheme());
	}


	// get username

	BufferedReader inp =
		    new BufferedReader(new InputStreamReader(System.in));
	System.out.print("Username: "); System.out.flush();
	try
	    { user = inp.readLine(); }
	catch (IOException ioe)
	    { return null; }
	if (user == null  ||  user.length() == 0)
	    return null;		// cancel'd


	// get password

	echo(false);
	System.out.print("Password: "); System.out.flush();
	try
	    { pass = inp.readLine(); }
	catch (IOException ioe)
	    { return null; }
	System.out.println();
	echo(true);

	if (pass == null)
	    return null;		// cancel'd


	// done

	return new NVPair(user, pass);
    }


    /*
     * Turn command-line echoing of typed characters on or off.
     */
    private static void echo(boolean on)
    {
	String os = System.getProperty("os.name");
	String[] cmd = null;

	if (os.equalsIgnoreCase("Windows 95")  ||
	    os.equalsIgnoreCase("Windows NT"))
	    // I don't think this works on M$ ...
	    cmd = new String[] { "echo", on ? "on" : "off" };
	else if (os.equalsIgnoreCase("Windows")  ||
		 os.equalsIgnoreCase("16-bit Windows"))
	    ;	// ???
	else if (os.equalsIgnoreCase("OS/2"))
	    ;	// ???
	else if (os.equalsIgnoreCase("Mac OS")  ||
		 os.equalsIgnoreCase("MacOS"))
	    ;	// ???
	else if (os.equalsIgnoreCase("OpenVMS") ||
		 os.equalsIgnoreCase("VMS"))
	    cmd = new String[] { "SET TERMINAL " + (on ? "/ECHO" : "/NOECHO") };
	else			// probably unix
	    cmd = new String[] { "/bin/sh", "-c",
				 "stty " + (on ? "echo" : "-echo") + " < /dev/tty" };

        if (cmd != null)
	    try
		{ Runtime.getRuntime().exec(cmd).waitFor(); }
	    catch (Exception e)
		{ }
    }

    /**
     * @return true for Unix's and VMS
     */
    static boolean canUseCLPrompt() {
	String os = System.getProperty("os.name");

	return (os.indexOf("Linux") >= 0    ||  os.indexOf("SunOS") >= 0  ||
		os.indexOf("Solaris") >= 0  ||  os.indexOf("BSD") >= 0    ||
		os.indexOf("AIX") >= 0      ||  os.indexOf("HP-UX") >= 0  ||
		os.indexOf("IRIX") >= 0     ||  os.indexOf("OSF") >= 0    ||
		os.indexOf("A/UX") >= 0     ||  os.indexOf("VMS") >= 0);
    }
}
