/*
 * @(#)HTTPConnection.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.io.*;
import java.net.*;
import java.util.*;
import java.applet.Applet;


/**
 * This class implements http protocol requests; it contains most of HTTP/1.1
 * and ought to be unconditionally compliant.
 * Redirections are automatically handled, and authorizations requests are
 * recognized and dealt with via an authorization handler.
 * Only full HTTP/1.0 and HTTP/1.1 requests are generated. HTTP/1.1, HTTP/1.0
 * and HTTP/0.9 responses are recognized.
 *
 * <P>Using the HTTPClient should be quite simple. First add the import
 * statement '<code>import HTTPClient.*;</code>' to your file(s). Request
 * can then be sent using one of the methods <var>Head()</var>,
 * <var>Get()</var>, <var>Post()</var>, etc in <var>HTTPConnection</var>.
 * These methods all return an instance of <var>HTTPResponse</var> which
 * has methods for accessing the response headers (<var>getHeader()</var>,
 * <var>getHeaderAsInt()</var>, etc), various response info
 * (<var>getStatusCode()</var>, <var>getReasonLine()</var>, etc) and the
 * reponse data (<var>getData()</var> and <var>getInputStream()</var>).
 * Following are some examples.
 * 
 * <P>If this is in an applet you can retrieve files from your server
 * as follows:
 * 
 * <pre>
 *     HTTPConnection con = new HTTPConnection(this);
 *     HTTPResponse   rsp = con.Get("/my_file");
 *     if (rsp.getStatusCode() >= 300)
 *     {
 * 	System.err.println("Received Error: "+rsp.getReasonLine());
 * 	System.err.println(new String(rsp.getData(),0));
 *     }
 *     else
 * 	data = rsp.getData();
 * 
 *     rsp = con.Get("/another_file");
 *     if (rsp.getStatusCode() >= 300)
 *     {
 * 	System.err.println("Received Error: "+rsp.getReasonLine());
 * 	System.err.println(new String(rsp.getData(),0));
 *     }
 *     else
 * 	other_data = rsp.getData();
 * </pre>
 * 
 * This will get the files "/my_file" and "/another_file" and put their
 * contents into byte[]'s accessible via <code>getData()</code>. Note that
 * you need to only create a new <var>HTTPConnection</var> when sending a
 * request to a new server (different host or port); although you may create
 * a new <var>HTTPConnection</var> for every request to the same server this
 * <strong>not</strong> recommended, as various information about the server
 * is cached after the first request (to optimize subsequent requests) and
 * persistent connections are used whenever possible.
 * 
 * <P>To POST form data you would use something like this (assuming you
 * have two fields called <var>name</var> and <var>e-mail</var>, whose
 * contents are stored in the variables <var>name</var> and <var>email</var>):
 * 
 * <pre>
 *     NVPair form_data[] = new NVPair[2];
 *     form_data[0] = new NVPair("name", name);
 *     form_data[1] = new NVPair("e-mail", email);
 * 
 *     HTTPConnection con = new HTTPConnection(this);
 *     HTTPResponse   rsp = con.Post("/cgi-bin/my_script", form_data);
 *     if (rsp.getStatusCode() >= 300)
 *     {
 * 	System.err.println("Received Error: "+rsp.getReasonLine());
 * 	System.err.println(new String(rsp.getData(),0));
 *     }
 *     else
 *         stream = rsp.getInputStream();
 * </pre>
 * 
 * Here the response data is read at leasure via an <var>InputStream</var>
 * instead of all at once into a <var>byte[]</var>.
 * 
 * <P>As another example, if you have a URL you're trying to send a request
 * to you would do something like the following:
 * 
 * <pre>
 *     URL url = new URL("http://www.mydomain.us/test/my_file");
 *     HTTPConnection con = new HTTPConnection(url);
 *     HTTPResponse   rsp = con.Put(url.getFile(), "Hello World");
 *     if (rsp.getStatusCode() >= 300)
 *     {
 * 	System.err.println("Received Error: "+rsp.getReasonLine());
 * 	System.err.println(new String(rsp.getData(),0));
 *     }
 *     else
 *         data = rsp.getData();
 * </pre>
 * 
 * <P>There are a whole number of methods for each request type; however the
 * general form's are:
 * <ul>
 * <li> Head ( file [, form-data [, headers ] ] )
 * <li> Head ( file [, query [, headers ] ] )
 * <li> Get ( file [, form-data [, headers ] ] )
 * <li> Get ( file [, query [, headers ] ] )
 * <li> Post ( file [, data [, headers ] ] )
 * <li> Put ( file [, data [, headers ] ] )
 * <li> Delete ( file [, headers ] )
 * <li> Options ( file [, headers ] )
 * <li> Trace ( file [, headers ] )
 * </ul>
 *
 * @version	0.2 (bug fix 2)  23/03/1997
 * @author	Ronald Tschal&auml;r
 */

public class HTTPConnection
{
    /** Current version */
    public static final String   version = "RPT-HTTPClient/0.2-2";

    /** The protocol used on this connection */
    private int                  Protocol;

    /** possible protocols */
    private final static int	 HTTP    = 0,
			    	 HTTPS   = 1,
			    	 HTTP_NG = 2;

    /** The protocol version */
            String		 ProtocolVersion;

    /** Have we settled on a protocol version? */
            boolean		 ProtVersFixed;

    /** The remote host this connection is associated with */
    private String               Host;

    /** The remote port this connection is attached to */
    private int                  Port;

    /** The current proxy host to use (if any) */
    private String               Proxy_Host = null;

    /** The current proxy port */
    private int                  Proxy_Port;

    /** The default proxy host to use (if any) */
    private static String        Default_Proxy_Host = null;

    /** The default proxy port */
    private static int           Default_Proxy_Port;

    /** The socks server to use */
    private SocksClient          Socks_client = null;

    /** The default socks server to use */
    private static SocksClient   Default_Socks_client = null;

    /** the stream demultiplexor */
    StreamDemultiplexor          input_demux = null;

    /** does the server support pipelining of requests? */
    boolean                      CanPipeline = false;

    /** have we been able to determine the above yet? */
    boolean			 PipeliningUnknown = true;

    /** the maximum number of requests over a HTTP/1.0 keep-alive connection */
    private int                  KeepAliveReqMax = -1;

    /** the number of requests over a HTTP/1.0 keep-alive connection left */
    private int                  KeepAliveReqLeft;

    /** The list of default http headers */
    private NVPair[]		 DefaultHeaders = new NVPair[0];

    /** Raw mode disables the handling of various response status codes */
    private boolean              RawMode = false;

    /** debuging variable: if true causes trace info to be printed out */
    static final boolean         debug = false;


    static
    {
	/*
	 * Let's try and see if we can figure out whether any proxies are
	 * being used.
	 */
	try
	{
	    String proxy = System.getProperty("proxySet");
	    if (proxy != null  &&  proxy.equalsIgnoreCase("true"))
	    {
		String host = System.getProperty("proxyHost");
		int    port = Integer.getInteger("proxyPort", -1).intValue();
		setProxyServer(host, port);
	    }
	}
	catch (Exception e)
	    { Default_Proxy_Host = null; }

	/*
	 * we can't turn the JDK SOCKS handling off, so we don't use the
	 * properties 'socksProxyHost' and 'socksProxyPort'. Instead we
	 * define 'socksHost', 'socksPort' and 'socksVersion'.
	 */
	try
	{
	    String host = System.getProperty("socksHost");
	    if (host != null  &&  host.length() > 0)
	    {
		int port    = Integer.getInteger("socksPort", -1).intValue();
		int version = Integer.getInteger("socksVersion", -1).intValue();
		if (version == -1)
		    setSocksServer(host, port);
		else
		    setSocksServer(host, port, version);
	    }
	}
	catch (Exception e)
	    { Default_Socks_client = null; }
    }


    // Constructors

    /**
     * Constructs a connection to the host from where the applet was loaded.
     * Note that current security policies only let applets connect home.
     *
     * @param applet the current applet
     */
    public HTTPConnection(Applet applet)  throws ProtocolNotSupportedException
    {
	this(applet.getCodeBase().getProtocol(),
	     applet.getCodeBase().getHost(),
	     applet.getCodeBase().getPort());
    }

    /**
     * Constructs a connection to the specified host on port 80
     *
     * @param host the host
     */
    public HTTPConnection(String host)
    {
	Setup(HTTP, host, 80);
    }

    /**
     * Constructs a connection to the specified host on the specified port
     *
     * @param host the host
     * @param port the port
     */
    public HTTPConnection(String host, int port)
    {
	Setup(HTTP, host, port);
    }

    /**
     * Constructs a connection to the specified host on the specified port,
     * using the specified protocol (currently only "http" is supported).
     *
     * @param prot the protocol
     * @param host the host
     * @param port the port
     * @exception MalformedURLException if the protocol is not HTTP
     */
    public HTTPConnection(String prot, String host, int port)  throws
	ProtocolNotSupportedException
    {
	if (!prot.equalsIgnoreCase("http"))
	    throw new ProtocolNotSupportedException("Unsupported protocol '" + prot + "'");

	if (prot.trim().equalsIgnoreCase("http"))
	    Setup(HTTP, host, port);
	else if (prot.trim().equalsIgnoreCase("https"))
	    Setup(HTTPS, host, port);
	else if (prot.trim().equalsIgnoreCase("http-ng"))
	    Setup(HTTP_NG, host, port);
    }

    /**
     * Constructs a connection to the host (port) as given in the url.
     *
     * @param     url the url
     * @exception ProtocolNotSupportedException if the protocol is not HTTP
     */
    public HTTPConnection(URL url) throws ProtocolNotSupportedException
    {
	this(url.getProtocol(), url.getHost(), url.getPort());
    }

    /**
     * Sets the class variables. Must not be public.
     *
     * @param prot the protocol
     * @param host the host
     * @param port the port
     */
    private void Setup(int prot, String host, int port)
    {
	Protocol = prot;
	Host     = host.trim();
	Port     = port;

	if (Port == -1)
	{
	    if (Protocol == HTTP  ||  Protocol == HTTP_NG)
		Port = 80;
	    else if (Protocol == HTTPS)
		Port = 443;
	}

	switch(Protocol)
	{
	    case HTTP:
		ProtocolVersion = "HTTP/1.1";
		ProtVersFixed   = false;
		break;
	    case HTTP_NG:
		ProtocolVersion = "Unknown";
		ProtVersFixed   = false;
		break;
	    case HTTPS:
		ProtocolVersion = "Unknown";
		ProtVersFixed   = false;
		break;
	    default:
		throw new Error("Internal Error: invalid protocol "+Protocol);
	}

	Proxy_Host   = Default_Proxy_Host;
	Proxy_Port   = Default_Proxy_Port;
	Socks_client = Default_Socks_client;
    }


    // Methods

    /**
     * Sends the HEAD request. This request is just like the corresponding
     * GET except that it only returns the headers and no data.
     *
     * @see #Get(java.lang.String)
     * @param     file the absolute path of the file
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Head(String file)
		throws IOException, AuthTypeNotImplementedException
    {
	return Head(file, (String) null, null);
    }

    /**
     * Sends the HEAD request. This request is just like the corresponding
     * GET except that it only returns the headers and no data.
     *
     * @see #Get(java.lang.String, HTTPClient.NVPair[])
     * @param     file      the absolute path of the file
     * @param     form_data an array of Name/Value pairs
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Head(String file, NVPair form_data[])
		throws IOException, AuthTypeNotImplementedException
    {
	return Head(file, form_data, null);
    }

    /**
     * Sends the HEAD request. This request is just like the corresponding
     * GET except that it only returns the headers and no data.
     *
     * @see #Get(java.lang.String, HTTPClient.NVPair[], HTTPClient.NVPair[])
     * @param     file      the absolute path of the file
     * @param     form_data an array of Name/Value pairs
     * @param     headers   additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Head(String file, NVPair[] form_data, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	String File  = stripRef(file),
	       query = Codecs.nv2query(form_data);
	if (query != null  &&  query.length() > 0)
	    File += "?" + query;

	return handleRequest("HEAD", File, headers, null, 0);
    }

    /**
     * Sends the HEAD request. This request is just like the corresponding
     * GET except that it only returns the headers and no data.
     *
     * @see #Get(java.lang.String, java.lang.String)
     * @param     file   the absolute path of the file
     * @param     query  the query string
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Head(String file, String query)
		throws IOException, AuthTypeNotImplementedException
    {
	return Head(file, query, null);
    }


    /**
     * Sends the HEAD request. This request is just like the corresponding
     * GET except that it only returns the headers and no data.
     *
     * @see #Get(java.lang.String, java.lang.String, HTTPClient.NVPair[])
     * @param     file    the absolute path of the file
     * @param     query   a query
     * @param     headers additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Head(String file, String query, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	String File = stripRef(file);
	if (query != null  &&  query.length() > 0)
	    File += "?" + Codecs.URLEncode(query);

	return handleRequest("HEAD", File, headers, null, 0);
    }


    /**
     * GET's the file.
     *
     * @param     file the absolute path of the file
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Get(String file)
		throws IOException, AuthTypeNotImplementedException
    {
	return Get(file, (String) null, null);
    }

    /**
     * GET's the file with a query consisting of the specified form-data.
     * The data is urlencoded, turned into a string of the form
     * "name1=value1&name2=value2" and then sent as a query string.
     *
     * @param     file       the absolute path of the file
     * @param     form_data  an array of Name/Value pairs
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Get(String file, NVPair form_data[])
		throws IOException, AuthTypeNotImplementedException
    {
	return Get(file, form_data, null);
    }

    /**
     * GET's the file with a query consisting of the specified form-data.
     * The data is urlencoded, turned into a string of the form
     * "name1=value1&name2=value2" and then sent as a query string.
     *
     * @param     file       the absolute path of the file
     * @param     form_data  an array of Name/Value pairs
     * @param     headers    additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Get(String file, NVPair[] form_data, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	String File  = stripRef(file),
	       query = Codecs.nv2query(form_data);
	if (query != null  &&  query.length() > 0)
	    File += "?" + query;

	return handleRequest("GET", File, headers, null, 0);
    }

    /**
     * GET's the file using the specified query string. The query string
     * is first urlencoded.
     *
     * @param     file  the absolute path of the file
     * @param     query the query
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Get(String file, String query)
		throws IOException, AuthTypeNotImplementedException
    {
	return Get(file, query, null);
    }

    /**
     * GET's the file using the specified query string. The query string
     * is first urlencoded.
     *
     * @param     file     the absolute path of the file
     * @param     query    the query
     * @param     headers  additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Get(String file, String query, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	String File = stripRef(file);
	if (query != null  &&  query.length() > 0)
	    File += "?" + URLEncoder.encode(query);

	return handleRequest("GET", File, headers, null, 0);
    }


    /**
     * POST's to the specified file. No data is sent.
     *
     * @param     file the absolute path of the file
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Post(String file)
		throws IOException, AuthTypeNotImplementedException
    {
	return Post(file, (byte []) null, null);
    }

    /**
     * POST's form-data to the specified file. The data is first urlencoded
     * and then turned into a string of the form "name1=value1&name2=value2".
     *
     * @param     file      the absolute path of the file
     * @param     form_data an array of Name/Value pairs
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Post(String file, NVPair form_data[])
		throws IOException, AuthTypeNotImplementedException
    {
	NVPair[] headers =
	    { new NVPair("Content-type", "application/x-www-form-urlencoded") };

	return Post(file, Codecs.nv2query(form_data), headers);
    }

    /**
     * POST's the data to the specified file. The data is converted to an
     * array of bytes using the lower byte of each character.
     * The request is sent using the content-type "application/octet-stream".
     *
     * @see java.lang.String#getBytes(int, int, byte[], int)
     * @param     file the absolute path of the file
     * @param     data the data
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Post(String file, String data)
		throws IOException, AuthTypeNotImplementedException
    {
	return Post(file, data, null);
    }

    /**
     * POST's the data to the specified file using the specified headers.
     *
     * @param     file     the absolute path of the file
     * @param     data     the data
     * @param     headers  additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Post(String file, String data, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	byte tmp[] = null;

	if (data != null  &&  data.length() > 0)
	{
	    tmp = new byte[data.length()];
	    data.getBytes(0, data.length(), tmp, 0);
	}

	return Post(file, tmp, headers);
    }

    /**
     * POST's the raw data to the specified file.
     * The request is sent using the content-type "application/octet-stream"
     *
     * @param     file the absolute path of the file
     * @param     data the data
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Post(String file, byte data[])
		throws IOException, AuthTypeNotImplementedException
    {
	return Post(file, data, null);
    }

    /**
     * POST's the raw data to the specified file using the specified headers.
     *
     * @param     file     the absolute path of the file
     * @param     data     the data
     * @param     headers  additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Post(String file, byte data[], NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	return handleRequest("POST", stripRef(file), headers, data, 0);
    }


    /**
     * PUT's the data into the specified file. The data is converted to an
     * array of bytes using the lower byte of each character.
     * The request ist sent using the content-type "application/octet-stream".
     *
     * @see java.lang.String#getBytes(int, int, byte[], int)
     * @param     file the absolute path of the file
     * @param     data the data
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Put(String file, String data)
		throws IOException, AuthTypeNotImplementedException
    {
	return Put(file, data, null);
    }

    /**
     * PUT's the data into the specified file using the additional headers
     * for the request.
     *
     * @param     file     the absolute path of the file
     * @param     data     the data
     * @param     headers  additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Put(String file, String data, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	byte tmp[] = null;

	if (data != null)
	{
	    tmp = new byte[data.length()];
	    data.getBytes(0, data.length(), tmp, 0);
	}

	return Put(file, tmp, headers);
    }

    /**
     * PUT's the raw data into the specified file.
     * The request is sent using the content-type "application/octet-stream".
     *
     * @param     file     the absolute path of the file
     * @param     data     the data
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Put(String file, byte data[])
		throws IOException, AuthTypeNotImplementedException
    {
	return Put(file, data, null);
    }

    /**
     * PUT's the raw data into the specified file using the additional
     * headers.
     *
     * @param     file     the absolute path of the file
     * @param     data     the data
     * @param     headers  any additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Put(String file, byte data[], NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	return handleRequest("PUT", stripRef(file), headers, data, 0);
    }


    /**
     * Request OPTIONS from the server. If <var>file</var> is "*" then
     * the request applies to the server as a whole; otherwise it applies
     * only to that resource.
     *
     * @param     file     the absolute path of the resource, or "*"
     * @param     headers  the headers containing optional info.
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Options(String file, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	return handleRequest("OPTIONS", stripRef(file), headers, null, 0);
    }


    /**
     * Request OPTIONS from the server. If <var>file</var> is "*" then
     * the request applies to the server as a whole; otherwise it applies
     * only to that resource.
     *
     * @param     file     the absolute path of the resource, or "*"
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Options(String file)
		throws IOException, AuthTypeNotImplementedException
    {
	return Options(file, null);
    }


    /**
     * Requests that <var>file</var> be DELETEd from the server.
     *
     * @param     file     the absolute path of the resource
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Delete(String file)
		throws IOException, AuthTypeNotImplementedException
    {
	return Delete(file, null);
    }


    /**
     * Requests that <var>file</var> be DELETEd from the server.
     *
     * @param     file     the absolute path of the resource
     * @param     headers  additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Delete(String file, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	return handleRequest("DELETE", stripRef(file), headers, null, 0);
    }


    /**
     * Requests a TRACE. Headers of particular interest here are "Via"
     * and "Max-Forwards".
     *
     * @param     file     the absolute path of the resource
     * @param     headers  additional headers
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Trace(String file, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	return handleRequest("TRACE", stripRef(file), headers, null, 0);
    }


    /**
     * Requests a TRACE.
     *
     * @param     file     the absolute path of the resource
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse Trace(String file)
		throws IOException, AuthTypeNotImplementedException
    {
	return Trace(file, null);
    }


    /**
     * This is here to allow an arbitrary, non-standard request to be sent.
     * I'm assuming you know what you are doing...
     *
     * @param     method   the extension method
     * @param     file     the absolute path of the resource, or null
     * @param     data     optional data, or null
     * @param     headers  optional headers, or null
     * @return    a HTTPResponse structure containing the response
     * @exception java.io.IOException when an exception is returned from
     *                                the socket.
     * @exception AuthTypeNotImplementedException when the server requests
     *                                authorization using a scheme that the
     *                                authorization handler does not handle.
     */
    public HTTPResponse ExtensionMethod(String method, String file,
					byte[] data, NVPair[] headers)
		throws IOException, AuthTypeNotImplementedException
    {
	return handleRequest(method.trim(), stripRef(file), headers, data, 0);
    }


    /**
     * Sets the default http headers to be sent with each request. The
     * actual headers sent are determined as follows: for each header
     * specified in multiple places a value given as part of the request
     * takes priority over any default values set by this method, which
     * in turn takes priority over any built-in default values. A different
     * way of looking at it is that we start off with a list of all headers
     * specified with the request, then add any default headers set by this
     * method which aren't already in our list, and finally add any built-in
     * headers which aren't yet in the list. There are two exceptions to this
     * rule: "Content-length", "Host" and "Authorization" headers are always
     * ignored; and when posting form-data any default "Content-type" is
     * ignored in favor of the built-in "application/x-www-form-urlencoded"
     * (however it will be overriden by any content-type header specified as
     * part of the request).
     *
     * <P>Typical headers you might want to set here are "Accept" and
     * "Connection".
     *
     * @param headers an array of header-name/value pairs (do not give the
     *                separating ':').
     */
    public void setDefaultHeaders(NVPair[] headers)
    {
	int length = (headers == null ? 0 : headers.length);
	DefaultHeaders = new NVPair[length];

	// weed out undesired headers
	int sidx, didx;
	for (sidx=0, didx=0; sidx<length; sidx++)
	{
	    if (headers[sidx].name.trim().equalsIgnoreCase("Content-length")  ||
		headers[sidx].name.trim().equalsIgnoreCase("Host")  ||
		headers[sidx].name.trim().equalsIgnoreCase("Proxy-Authorization")  ||
		headers[sidx].name.trim().equalsIgnoreCase("Authorization"))
		continue;
	    
	    DefaultHeaders[didx++] = headers[sidx];
	}

	if (didx < length)
	    DefaultHeaders = Util.resizeArray(DefaultHeaders, didx);
    }


    /**
     * Gets the current list of default http headers.
     *
     * @return an array of header/value pairs.
     */
    public NVPair[] getDefaultHeaders()
    {
	NVPair[] headers = new NVPair[DefaultHeaders.length];
	System.arraycopy(DefaultHeaders, 0, headers, 0, headers.length);
	return headers;
    }


    /**
     * Sets/Resets raw mode. In raw mode the automatic handling of
     * authorization requests, redirections and certain other response
     * status codes is turned off. The default is false.
     *
     * @param flag if true turns off handling of certain response status codes.
     */
    public void setRawMode(boolean flag)
    {
	RawMode = flag;
    }


    /**
     * 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 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 void addAuthorization(String scheme, String realm, String cookie,
				 NVPair params[])
    {
	AuthorizationInfo.addAuthorization(Host, Port, scheme, realm, cookie,
					   params);
    }


    /**
     * 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 realm the realm
     * @param user  the username
     * @param passw the password
     */
    public void addBasicAuthorization(String realm, String user, String passw)
    {
	AuthorizationInfo.addBasicAuthorization(Host, Port, realm, user, passw);
    }


    /**
     * Set's the default proxy server to use. The proxy will only be used
     * for new <var>HTTPConnection</var>s created after this call and will
     * not affect currrent instances of <var>HTTPConnection</var>. A null
     * or empty string <var>host</var> parameter disables the proxy.
     *
     * <P>In an application or using the Appletviewer an alternative to
     * this method is to set the following properties (either in the
     * properties file or on the command line):
     * <var>proxySet</var>, <var>proxyHost</var> and <var>proxyPort</var>.
     * Whether <var>proxySet</var> is set to "true" or not determines whether
     * a proxy server is used; if <var>proxySet</var> is "true" then both
     * <var>proxyHost</var> and <var>proxyPort</var> must also be set.
     *
     * <P>If the proxy server requires authorization and you wish to set
     * this authorization information in the code, then you may use the
     * <var>AuthorizationInfo.addAuthorization()</var> and
     * <var>AuthorizationInfo.addBasicAuthorization()</var> methods to
     * do so. Specify the same <var>host</var> and <var>port</var> as in
     * this method. If you have not given any authorization info and the
     * proxy server requires authorization then you will be prompted for
     * the necessary info via a popup the first time you do a request.
     *
     * @param  host    the host on which the proxy server resides.
     * @param  port    the port the proxy server is listening on.
     */
    public static void setProxyServer(String host, int port)
    {
	if (host == null  ||  host.length() == 0)
	    Default_Proxy_Host = null;
	else
	{
	    Default_Proxy_Host = host.trim();
	    Default_Proxy_Port = port;
	    if (Default_Proxy_Port <= 0)  Default_Proxy_Port = 8000;	// ???
	}
    }


    /**
     * Set's the SOCKS server to use. The server will only be used
     * for new HTTPConnections created after this call and will not affect
     * currrent instances of HTTPConnection. A null or empty string host
     * parameter disables SOCKS.
     * <P>The code will try to determine the SOCKS version to use at
     * connection time. This might fail for a number of reasons, however,
     * in which case you must specify the version explicitly.
     *
     * @see HTTPClient.HTTPConnection#setSocksServer(java.lang.String, int, int)
     * @param  host    the host on which the proxy server resides. The port
     *                 used is the default port 1080.
     */
    public static void setSocksServer(String host)
    {
	setSocksServer(host, 1080);
    }


    /**
     * Set's the SOCKS server to use. The server will only be used
     * for new HTTPConnections created after this call and will not affect
     * currrent instances of HTTPConnection. A null or empty string host
     * parameter disables SOCKS.
     * <P>The code will try to determine the SOCKS version to use at
     * connection time. This might fail for a number of reasons, however,
     * in which case you must specify the version explicitly.
     *
     * @see HTTPClient.HTTPConnection#setSocksServer(java.lang.String, int, int)
     * @param  host    the host on which the proxy server resides.
     * @param  port    the port the proxy server is listening on.
     */
    public static void setSocksServer(String host, int port)
    {
	if (port <= 0)
	    port = 1080;

	if (host == null  ||  host.length() == 0)
	    Default_Socks_client = null;
	else
	    Default_Socks_client = new SocksClient(host, port);
    }


    /**
     * Set's the SOCKS server to use. The server will only be used
     * for new HTTPConnections created after this call and will not affect
     * currrent instances of HTTPConnection. A null or empty string host
     * parameter disables SOCKS.
     *
     * <P>In an application or using the Appletviewer an alternative to
     * this method is to set the following properties (either in the
     * properties file or on the command line):
     * <var>socksHost</var>, <var>socksPort</var> and <var>socksVersion</var>.
     * Whether <var>socksHost</var> is set or not determines whether a
     * SOCKS server is used; if <var>socksPort</var> is not set it defaults
     * to 1080; if <var>socksVersion</var> is not set an attempt will be
     * made to automatically determine the version used by the server.
     *
     * <P>Note: If you have also set a proxy server then a connection
     * will be made to the SOCKS server, which in turn then makes a
     * connection to the proxy server (possibly via other SOCKS servers),
     * which in turn makes the final connection.
     *
     * <P>If the proxy server is running SOCKS version 5 and requires
     * username/password authorization, and you wish to set
     * this authorization information in the code, then you may use the
     * <var>AuthorizationInfo.addAuthorization()</var> method to do so.
     * Specify the same <var>host</var> and <var>port</var> as in this
     * method, give the <var>scheme</var> "SOCKS5" and the <var>realm</var>
     * "USER/PASS", set the <var>cookie</var> to null and the
     * <var>params</var> to an array containing a single <var>NVPair</var>
     * in turn containing the username and password. Example:
     * <pre>
     *     NVPair[] up = { new NVPair(username, password) };
     *     AuthorizationInfo.addAuthorization(host, port, "SOCKS5", "USER/PASS",
     *                                        null, up);
     * </pre>
     * If you have not given any authorization info and the proxy server
     * requires authorization then you will be prompted for the necessary
     * info via a popup the first time you do a request.
     *
     * @param  host    the host on which the proxy server resides.
     * @param  port    the port the proxy server is listening on.
     * @param  version the SOCKS version the server is running. Currently
     *                 this must be '4' or '5'.
     * @exception SocksException If <var>version</var> is not '4' or '5'.
     */
    public static void setSocksServer(String host, int port, int version)
	    throws SocksException
    {
	if (port <= 0)
	    port = 1080;

	if (host == null  ||  host.length() == 0)
	    Default_Socks_client = null;
	else
	    Default_Socks_client = new SocksClient(host, port, version);
    }


    private final String stripRef(String file)
    {
	if (file == null)  file = "/";

	int hash = file.indexOf('#');
	if (hash != -1)
	    file = file.substring(0,hash).trim();
	else
	    file = file.trim();

	if (file.length() == 0)  file = "/";

	return file;
    }


    // private helper methods

    /**
     * handles a request, including things like authorization and redirection
     *
     * @param  method  GET, POST, etc.
     * @param  file    the file
     * @param  headers an array of headers to be used
     * @param  data    the data
     * @return the final response. Note that in case of redirection the
     *         EffectiveURL field will point to the URL from which the
     *         actual data was returned.
     */
    private HTTPResponse handleRequest(String method, String file,
				       NVPair[] headers, byte[] data, int lvl)
		throws IOException, AuthTypeNotImplementedException
    {
	HTTPResponse         resp = null;
	boolean              done = false;
	NVPair[]             merged_hdrs = mergedHeaders(headers);
	int		     sts,
			     req_timeout_retries = 3,
			     hdr_auth_idx = -1,
			     hdr_prxy_idx = -1,
			     lst_auth_idx = 0,
			     lst_prxy_idx = 0,
			     scm_auth_idx = 0,
			     scm_prxy_idx = 0;


	while(!done)
	{
	    resp = sendRequest(method, file, merged_hdrs, data);
	    if (RawMode)  break;

	    sts  = resp.getStatusCode();

	    if (debug) System.err.println("Received Status: " + sts + " " +
					   resp.getReasonLine());

	    switch(sts)
	    {
		case 301: // Moved Permanently
		case 302: // Moved Temporarily
		case 303: // See Other

		    // the spec says redirection may only be done if the
		    // second request is a HEAD or GET.
		    if (!method.equalsIgnoreCase("GET")  &&
			!method.equalsIgnoreCase("HEAD")  &&
			sts != 303)
		    {
			done = true;
			break;
		    }

		case 305: // Use Proxy

		    URL loc = resp.getEffectiveURL();

		    // the level is a primitive way of preventing infinite
		    // redirections
		    if (lvl == 5  ||  loc == null)
		    {
			done = true;
			break;
		    }

		    HTTPConnection mvd;
		    String nfile;
		    if (sts == 305)
		    {
			mvd = new HTTPConnection(Host, Port);
			mvd.Protocol   = this.Protocol;
			mvd.Proxy_Host = loc.getHost();
			mvd.Proxy_Port = loc.getPort();
			if (mvd.Proxy_Port == -1)  mvd.Proxy_Port = 8000; // ???
			nfile = file;
			resp.setEffectiveURL(null);
		    }
		    else
		    {
			mvd = new HTTPConnection(loc);

			nfile = resp.getEffectiveURL().getFile();
			int oquery = file.indexOf('?'),
			    nquery = nfile.indexOf('?');
			if (oquery != -1  &&  nquery == -1)
			    nfile += file.substring(oquery);
		    }

		    if (sts == 303  &&  !method.equalsIgnoreCase("HEAD"))
			method = "GET";

		    HTTPResponse m_resp =
			mvd.handleRequest(method, nfile, headers, data, lvl+1);

		    if (m_resp.getEffectiveURL() == null)
			m_resp.setEffectiveURL(resp.getEffectiveURL());

		    resp.getInputStream().close();
		    resp = m_resp;

		    done = true;
		    break;

		case 401: // Unauthorized
		case 407: // Proxy Authentication Required

		    if (sts == 401)
		    {
			int[] idx_arr = { lst_auth_idx,	// hack to pass by ref
					  scm_auth_idx,
					  hdr_auth_idx};

			merged_hdrs = setAuthHeaders(
			    resp.getHeader("WWW-Authenticate"), Host, Port,
			    merged_hdrs, "Authorization", idx_arr);

			lst_auth_idx = idx_arr[0];
			scm_auth_idx = idx_arr[1];
			hdr_auth_idx = idx_arr[2];
		    }
		    else
		    {
			int[] idx_arr = { lst_prxy_idx,	// hack to pass by ref
					  scm_prxy_idx,
					  hdr_prxy_idx};

			merged_hdrs = setAuthHeaders(
			    resp.getHeader("Proxy-Authenticate"), Proxy_Host,
			    Proxy_Port, merged_hdrs, "Proxy-Authorization",
			    idx_arr);

			lst_prxy_idx = idx_arr[0];
			scm_prxy_idx = idx_arr[1];
			hdr_prxy_idx = idx_arr[2];
		    }

		    if (merged_hdrs == null)
			done = true;
		    else
			resp.getInputStream().close();

		    break;

		case 408: // Request Timeout
		    if (req_timeout_retries-- == 0)
			done = true;
		    break;

		case 411: // Length Required
		    resp.getInputStream().close();
		    if (data != null)
			throw new ProtocolException("Received StatusCode 411 even though Content-Length was sent");

		    data = new byte[0];	// will send Content-Length: 0
		    break;

		case 505: // HTTP Version not supported
		    done = true;
		    break;

		default:
		    done = true;
		    break;
	    }

	}

	return resp;
    }


    /**
     * This merges built-in default headers, user-specified default headers,
     * and method-specified headers. Method-specified take precedence over
     * user defaults, which take precedence over built-in defaults.
     *
     * The following headers are removed if found: "Host", "Content-length",
     * "Authorization" and "Proxy-Authorization".
     *
     * @param  spec   the headers specified in the call to the method
     * @return an array consisting of merged headers.
     */
    private NVPair[] mergedHeaders(NVPair[] spec)
    {
	int spec_len = (spec != null ? spec.length : 0),
	    defs_len = (DefaultHeaders != null ? DefaultHeaders.length : 0);
	NVPair[] merged = new NVPair[spec_len + defs_len];
	int sidx, didx;

	// merge in default headers
	didx = defs_len;
	System.arraycopy(DefaultHeaders, 0, merged, 0, didx);

	// merge in selected headers
	for (sidx=0; sidx<spec_len; sidx++)
	{
	    if (spec[sidx].name.trim().equalsIgnoreCase("Content-length")  ||
		spec[sidx].name.trim().equalsIgnoreCase("Host")  ||
		spec[sidx].name.trim().equalsIgnoreCase("Proxy-Authorization")  ||
		spec[sidx].name.trim().equalsIgnoreCase("Authorization"))
		continue;

	    int search;
	    for (search=0; search<didx; search++)
	    {
		if (merged[search].name.trim().equalsIgnoreCase(
			spec[sidx].name.trim()))
		    break;
	    }

	    merged[search] = spec[sidx];
	    if (search == didx) didx++;
	}

	if (didx < merged.length)
	    merged = Util.resizeArray(merged, didx);

	return merged;
    }


    /**
     * Handles authorization requests and sets the authorization headers.
     * It tries to retrieve the neccessary parameters from AuthorizationInfo,
     * and failing that calls the AuthHandler. Handles multiple authorization
     * headers.
     *
     * @param  auth_str the authorization header field returned by the server.
     * @param  Host     the host to use in querying AuthorizationInfo.
     * @param  Port     the port to use in querying AuthorizationInfo.
     * @param  merged_headers  the headers used in the previous request.
     * @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.
     * @return the headers with the new auth paramters to try.
     * @exception ProtocolException if <var>auth_str</var> is null.
     * @exception AuthTypeNotImplementedException if thrown by the AuthHandler.
     */
    private NVPair[] setAuthHeaders(String auth_str, String Host, int Port,
				    NVPair[] merged_hdrs, String header,
				    int[] idx_arr)
	throws ProtocolException, AuthTypeNotImplementedException 
    {
	if (auth_str == null)
	    throw new ProtocolException("Missing Authorization header");

	AuthorizationInfo[] challenge =
		    AuthorizationInfo.parseAuthString(Host, Port, auth_str);

	AuthorizationInfo credentials = null;

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

	// if we don't have any credentials then prompt the user
	while (credentials == null  &&  idx_arr[1] != -1)
	{
	    try
	    {
		credentials =
		    AuthorizationInfo.queryAuthHandler(challenge[idx_arr[1]]);
	    }
	    catch (AuthTypeNotImplementedException atnie)
	    {
		if (idx_arr[1] == challenge.length-1)
		    throw atnie;
	    }
	    if (++idx_arr[1] == challenge.length)
		idx_arr[1] = -1;
	}

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

	// add credentials to headers
	if (idx_arr[2] == -1)
	{
	    merged_hdrs = Util.resizeArray(merged_hdrs, merged_hdrs.length+1);
	    idx_arr[2]  = merged_hdrs.length - 1;
	}
	merged_hdrs[idx_arr[2]] = new NVPair(header, credentials.toString());

	if (debug)
	    System.err.println("Using " + header + ": " + credentials);

	return merged_hdrs;
    }


    /**
     * sends the request over the line.
     *
     * Note: this might seem wasteful to keep opening/closing connections
     *       (and indeed it is), but this limited by http, which only allows
     *       *a single* request per connection.
     *       Subnote: Some servers accept the (faulty) keep-alive option;
     *                HTTP/1.1 introduces the persist option which allows
     *                multiple request per connection; support for this is
     *                on the to-do list.
     *
     * @param  method   GET, POST, etc.
     * @param  file     the resource
     * @param  headers  a list of headers
     * @param  data     the data
     * @return the response.
     * @exception java.io.IOException any exception the socket or IO-streams
     *                                throw.
     */
    private HTTPResponse sendRequest(String method, String file,
				     NVPair[] headers, byte[] data)
		throws IOException
    {
	ByteArrayOutputStream buff_out = new ByteArrayOutputStream(500);
	HTTPResponse          resp = null;
	boolean		      keep_alive;


	if (debug) System.err.println("\nDoing Request...");


	String[] con_hdrs =
		    assembleHeaders(method, file, headers, data, buff_out);


	// determine if the connection should be kept alive after this
	// request

	if (ProtocolVersion.equals("HTTP/1.1")  &&  (con_hdrs == null  ||
	     con_hdrs[0].indexOf("close") == -1)  
	    ||
	    ProtocolVersion.equals("HTTP/1.0")  &&  (con_hdrs != null  &&
	     con_hdrs[0].indexOf("keep-alive") != -1)  
	    )
	    keep_alive = true;
	else
	    keep_alive = false;


	// this part is single threaded

	synchronized (this)
	{
	    int try_count = 3;
	    // what a hack! The problem is that we only get IOException, but
	    // we need a finer specification (i.e. whether it's an EPIPE or
	    // something else); I don't trust relying on the message part
	    // of IOException (which on SunOS/Solaris gives 'Broken pipe',
	    // but what on Windoze/Mac?).

	    while (try_count-- > 0)
	    {
		try
		{
		    // get a client socket 

		    Socket sock;
		    if (input_demux == null  ||
			(sock = input_demux.getSocket()) == null)
		    {
			sock = getSocket();
			input_demux =
				new StreamDemultiplexor(Protocol, sock, this);
			KeepAliveReqLeft = KeepAliveReqMax;
		    }

		    if (debug)
			System.err.println("Sending Request: "+method+" "+file);


		    // Send headers

		    OutputStream sock_out = sock.getOutputStream();
		    buff_out.writeTo(sock_out);


		    // POST/PUT data

		    if (data != null  &&  data.length > 0)
			sock_out.write(data);
		}
		catch (IOException ioe)
		{
		    if (debug)
			ioe.printStackTrace();

		    try
			{ if (input_demux != null)  input_demux.close(ioe); }
		    catch (IOException ioe2)
			{ }

		    if (try_count == 0  ||
			ioe instanceof UnknownHostException)
			throw ioe;
		    else
			continue;
		}


		// get a new response. Note this does not do a read on the
		// socket.

		resp = new HTTPResponse(method, con_hdrs, buff_out, data,
					input_demux);


		// if this is the very first request to the server then
		// read response headers to get protocol version used by
		// the server.

		if (!ProtVersFixed)
		{
		    boolean downgraded = false;

		    if (resp.getVersion().equalsIgnoreCase("HTTP/0.9")  ||
			resp.getVersion().equalsIgnoreCase("HTTP/1.0"))
		    {
			ProtocolVersion = "HTTP/1.0";
			downgraded      = true;
		    }
		    else
			ProtocolVersion = "HTTP/1.1";

		    ProtVersFixed = true;

		    if (debug)
			System.err.println("Protocol Version established: "
					    + ProtocolVersion);
		    
		    if (resp.getStatusCode() == 505   // Version not supported
			// some servers return an error status if they get a
			// version they didn't comprehend
		       ||  (resp.getStatusCode() == 400  &&  downgraded)
		       ||  (resp.getStatusCode() == 500  &&  downgraded))
		    {
			input_demux.markForClose();
			input_demux = null;
			resp = sendRequest(method, file, headers, data);
			break;
		    }
		}


		// try and determine if this server allows pipelining

		if (PipeliningUnknown  &&  keep_alive)
		{
		    String con;

		    if (!ProtocolVersion.equals("HTTP/1.0")  ||
			((con = resp.getHeader("Connection")) != null  &&
			con.toLowerCase().indexOf("keep-alive") != -1))
		    {
			CanPipeline = true;

			if (debug)
			    System.err.println("Pipelining enabled");

			PipeliningUnknown = false;
		    }
		    else if (resp.getStatusCode() < 400)
			PipeliningUnknown = false;


		    // get maximum number of requests

		    if (CanPipeline  &&
			ProtocolVersion.equals("HTTP/1.0")  &&
			(con = resp.getHeader("Keep-Alive")) != null)
		    {
			int idx = con.toLowerCase().indexOf("max");
			if (idx != -1)
			    idx = con.indexOf('=', idx+3);
			if (idx != -1)
			{
			    int end = con.indexOf(',', idx+1);
			    if (end == -1)  end = con.length();
			    String max = con.substring(idx+1, end).trim();
			    try
				{ KeepAliveReqMax = Integer.parseInt(max); }
			    catch (NumberFormatException nfe)
				{ }
			    KeepAliveReqLeft = KeepAliveReqMax;
			}
		    }
		}

		if (!CanPipeline  ||  !keep_alive  ||
		    (KeepAliveReqMax != -1  &&  KeepAliveReqLeft-- == 0))
		{
		    input_demux.markForClose();
		    input_demux = null;
		}

		break;
	    }

	    if (input_demux != null)
		input_demux.restartTimer();

	    if (debug) System.err.println("Request done");
	}

	return resp;
    }


    /**
     * Gets a socket. Creates a socket to the proxy if set, or else to the
     * actual destination.
     */
    Socket getSocket()  throws IOException
    {
	Socket sock;

	if (debug) System.err.println("Creating Socket: "+Host+":"+Port);

	String actual_host;
	int    actual_port;

	if (Proxy_Host != null)
	{
	    actual_host = Proxy_Host;
	    actual_port = Proxy_Port;
	}
	else
	{
	    actual_host = Host;
	    actual_port = Port;
	}
	if (Socks_client != null)
	    sock = Socks_client.getSocket(actual_host, actual_port);
	else
	    sock = new Socket(actual_host, actual_port);

	return sock;
    }


    private String[] assembleHeaders(String method, String file,
				    NVPair[] headers, byte[] data,
				    ByteArrayOutputStream buff_out)
		throws IOException
    {
	DataOutputStream dataout = new DataOutputStream(buff_out);


	// Assemble HTTP headers

	if (Proxy_Host != null)
	{
	    if (file.equals("*"))
		file = "";      // the proxy will regenerate the '*'
	    dataout.writeBytes(method + " http://" + Host + ":" + Port +
			       file + " " + ProtocolVersion + "\r\n");
	}
	else
	    dataout.writeBytes(method + " " + file + " " + ProtocolVersion
			       + "\r\n");
	if (Port != 80)
	    dataout.writeBytes("Host: " + Host + ":" + Port + "\r\n");
	else	// Netscape-Enterprise has some bugs...
	    dataout.writeBytes("Host: " + Host + "\r\n");

	int ct_idx = -1,
	    ua_idx = -1,
	    co_idx = -1,
	    ka_idx = -1;
	for (int idx=0; idx<headers.length; idx++)
	{
	    if (headers[idx].name.trim().equalsIgnoreCase("Content-type"))
		ct_idx = idx;
	    if (headers[idx].name.trim().equalsIgnoreCase("User-Agent"))
		ua_idx = idx;
	    if (headers[idx].name.trim().equalsIgnoreCase("Connection"))
		co_idx = idx;
	    if (headers[idx].name.trim().equalsIgnoreCase("Keep-Alive"))
		ka_idx = idx;
	}

	if (ua_idx != -1)
	    dataout.writeBytes("User-Agent: " +
			       headers[ua_idx].value.trim() + " " +
			       version + "\r\n");
	else
	    dataout.writeBytes("User-Agent: " + version + "\r\n");

	for (int idx=0; idx<headers.length; idx++)
	{
	    if (idx == ct_idx  ||  idx == ua_idx  ||  idx == co_idx  ||
		idx == ka_idx)
		continue;
	    dataout.writeBytes(headers[idx].name.trim() + ": " +
			       headers[idx].value.trim() + "\r\n");
	}

	if (data != null)
	{
	    if (ct_idx != -1)
		dataout.writeBytes("Content-type: " +
				    headers[ct_idx].value.trim() + "\r\n");
	    else
		dataout.writeBytes("Content-type: application/octet-stream\r\n");
	    dataout.writeBytes("Content-length: " + data.length + "\r\n");
	}


	/*
	 * What follows is the setup for persistent connections. We default
	 * to doing persistent connections for both HTTP/1.0 and HTTP/1.1,
	 * unless we're using a proxy server and HTTP/1.0 in which case we
	 * must make sure we don't do persistence (because of the problem of
	 * 1.0 proxies blindly passing the keep-alive header on).
	 */
	String con;

	if (co_idx != -1)	// the user specified a connection header
	{
	    con = headers[co_idx].value.trim();

	    // remove keep-alive token if we're talking to a proxy and not
	    // sure that its version 1.1 or later
	    if (Proxy_Host != null  &&
		(ProtocolVersion.equals("HTTP/1.0")  ||  !ProtVersFixed))
	    {
		int ka = con.toLowerCase().indexOf("keep-alive");
		if (ka != -1)
		{
		    con = con.substring(0,ka).trim() +
			  con.substring(ka+10).trim();
		    if (con.charAt(0) == ',')
			con = con.substring(1).trim();
		    if (con.charAt(con.length()-1) == ',')
			con = con.substring(0, con.length()-1).trim();
		}
	    }
	}
	else		// no connection header given by user
	{
	    ka_idx = -1;

	    // add keep-alive token if we're not talking to a proxy and not
	    // sure that its version 1.1
	    if (Proxy_Host == null  &&
		(ProtocolVersion.equals("HTTP/1.0")  ||  !ProtVersFixed))
		con = "Keep-Alive";
	    else
		con = "";
	}

	String[] con_hdrs = null;

	if (con.length() > 0)
	{
	    dataout.writeBytes("Connection: " + con + "\r\n");
	    if (ka_idx != -1  &&  con.toLowerCase().indexOf("keep-alive") != -1)
	    {
		dataout.writeBytes("Keep-Alive: " +
				    headers[ka_idx].value.trim() + "\r\n");

		con_hdrs    = new String[2];
		con_hdrs[0] = con.toLowerCase();
		con_hdrs[1] = headers[ka_idx].value.trim().toLowerCase();
	    }
	    else
	    {
		con_hdrs    = new String[1];
		con_hdrs[0] = con.toLowerCase();
	    }
	}

	dataout.writeBytes("\r\n");		// end of header

	return con_hdrs;
    }

}

