/*
 * $Header: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v 1.23 2002/10/31 07:45:34 jsdever Exp $
 * $Revision: 1.23 $
 * $Date: 2002/10/31 07:45:34 $
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999-2002 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "HttpClient", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */

package org.apache.commons.httpclient;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.SocketException;
import javax.net.ssl.SSLSocketFactory;



/**
 * <p>
 * An abstraction of an HTTP {@link InputStream} and {@link OutputStream}
 * pair, together with the relevant attributes.
 * </p>
 * @author Rod Waldhoff
 * @author Sean C. Sullivan
 * @author Ortwin Glück
 * @version $Revision: 1.23 $ $Date: 2002/10/31 07:45:34 $
 */
public class HttpConnection {
    // ----------------------------------------------------------- Constructors

    /**
     * Constructor.
     *
     * @param host the host I should connect to
     * @param port the port I should connect to
     */
    public HttpConnection(String host, int port) {
        this(null, -1, host, port, false);
    }

    /**
     * Constructor.
     *
     * @param host the host I should connect to
     * @param port the port I should connect to
     * @param secure when <tt>true</tt>, connect via HTTPS (SSL)
     */
    public HttpConnection(String host, int port, boolean secure) {
        this(null, -1, host, port, secure);
    }

    /**
     * Constructor.
     *
     * @param proxyHost the host I should proxy via
     * @param proxyPort the port I should proxy via
     * @param host the host I should connect to
     * @param port the port I should connect to
     */
    public HttpConnection(String proxyHost, int proxyPort, String host, int port) {
        this(proxyHost, proxyPort, host, port, false);
    }

    /**
     * Fully-specified constructor.
     *
     * @param proxyHost the host I should proxy via
     * @param proxyPort the port I should proxy via
     * @param host the host I should connect to. Parameter value must be non-null.
     * @param port the port I should connect to
     * @param secure when <tt>true</tt>, connect via HTTPS (SSL)
     */
    public HttpConnection(String proxyHost, int proxyPort, String host, 
    int port, boolean secure) {
        if  (log.isDebugEnabled()){
            log.debug("HttpConnectionManager.getConnection:  creating "
                + " connection for " + host + ":" + port + " via " + proxyHost
                + ":" + proxyPort);
        }

        if (host == null) {
            throw new NullPointerException("host parameter is null");
        }
        _proxyHost = proxyHost;
        _proxyPort = proxyPort;
        _host = host;
        _port = port;
        _ssl = secure;
    }

    // ------------------------------------------ Attribute Setters and Getters

    /**
     * Specifies an alternative factory for SSL sockets. If <code>factory</code>
     * is <code>null</code> the default implementation is used.
     *
     * @param factory An instance of a SSLSocketFactory or <code>null</code>.
     * @throws IllegalStateException If called after the connection was opened
     */
    public void setSSLSocketFactory(SSLSocketFactory factory) {
        assertNotOpen();
        sslSocketFactory = factory;
    }

    /**
     * Return my host.
     *
     * @return my host.
     */
    public String getHost() {
        return _host;
    }

    /**
     * Set my host.
     *
     * @param host the host I should connect to. Parameter value must be non-null.
     * @throws IllegalStateException if I am already connected
     */
    public void setHost(String host) throws IllegalStateException {
        if (host == null) {
            throw new NullPointerException("host parameter is null");
        }
        assertNotOpen();
        _host = host;
    }

    /**
     * Return my port.
     *
     * If the port is -1 (or less than 0) the default port for
     * the current protocol is returned.
     *
     * @return my port.
     */
    public int getPort() {
        if (_port < 0) {
            return isSecure() ? 443 : 80;
        } else {
            return _port;
        }
    }

    /**
     * Set my port.
     *
     * @param port the port I should connect to
     * @throws IllegalStateException if I am already connected
     */
    public void setPort(int port) throws IllegalStateException {
        assertNotOpen();
        _port = port;
    }

    /**
     * Return my proxy host.
     *
     * @return my proxy host.
     */
    public String getProxyHost() {
        return _proxyHost;
    }

    /**
     * Set the host I should proxy through.
     *
     * @param host the host I should proxy through.
     * @throws IllegalStateException if I am already connected
     */
    public void setProxyHost(String host) throws IllegalStateException {
       assertNotOpen();
       _proxyHost = host;
    }

    /**
     * Return my proxy port.
     *
     * @return my proxy port.
     */
    public int getProxyPort() {
        return _proxyPort;
    }

    /**
     * Set the port I should proxy through.
     *
     * @param port the host I should proxy through.
     * @throws IllegalStateException if I am already connected
     */
    public void setProxyPort(int port) throws IllegalStateException {
       assertNotOpen();
       _proxyPort = port;
    }

    /**
     * Return <tt>true</tt> if I will (or I am) connected over a
     * secure (HTTPS/SSL) protocol.
     *
     * @return <tt>true</tt> if I will (or I am) connected over a
     *         secure (HTTPS/SSL) protocol.
     */
    public boolean isSecure() {
        return _ssl;
    }

    /**
     * Get the protocol.
     * @return HTTPS if secure, HTTP otherwise
     */
    public String getProtocol() {
        return (isSecure() ? "HTTPS" : "HTTP");
    }



    /**
     * Set whether or not I should connect over HTTPS (SSL).
     *
     * @param secure whether or not I should connect over HTTPS (SSL).
     * @throws IllegalStateException if I am already connected
     */
    public void setSecure(boolean secure) throws IllegalStateException {
        assertNotOpen();
        _ssl = secure;
    }

    /**
     * Return <tt>true</tt> if I am connected,
     * <tt>false</tt> otherwise.
     *
     * @return <tt>true</tt> if I am connected
     */
    public boolean isOpen() {
        return _open;
    }

    /**
     * Return <tt>true</tt> if I am (or I will be)
     * connected via a proxy, <tt>false</tt> otherwise.
     *
     * @return <tt>true</tt> if I am (or I will be)
     *         connected via a proxy, <tt>false</tt> otherwise.
     */
    public boolean isProxied() {
        return (!(null == _proxyHost || 0 >= _proxyPort));
    }

    // --------------------------------------------------- Other Public Methods

    /**
     * Set my {@link Socket}'s timeout, via {@link Socket#setSoTimeout}.  If the
     * connection is already open, the SO_TIMEOUT is changed.  If no connection
     * is open, then subsequent connections will use the timeout value.
     *
     * @param timeout the timeout value
     * @throws SocketException - if there is an error in the underlying
     * protocol, such as a TCP error.
     * @throws IllegalStateException if I am not connected
     */
    public void setSoTimeout(int timeout) 
    throws SocketException, IllegalStateException {
        log.debug("HttpConnection.setSoTimeout("+ timeout +")");
        _so_timeout = timeout;
        if(_socket != null){
            _socket.setSoTimeout(timeout);
        }
    }

    /**
     * Open this connection to the current host and port
     * (via a proxy if so configured).
     *
     * @throws IOException when there are errors opening the connection
     */
    public void open() throws IOException {
        log.trace("enter HttpConnection.open()");

        assertNotOpen(); // ??? is this worth doing?
        try {
            if (null == _socket) {
                String host = (null == _proxyHost) ? _host : _proxyHost;
                int port = (null == _proxyHost) ? _port : _proxyPort;
                if (isSecure() && !isProxied()) {
                    if (sslSocketFactory == null) {
                        sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
                    }
                    _socket = (new SocketCreator()).createSocket(host,port);
                    _usingSecureSocket = true;
                } else {
                    _socket = new Socket();
					_socket.connect(new java.net.InetSocketAddress(host,port),_so_timeout);
                    _usingSecureSocket = false;
                }
            }
            _socket.setSoTimeout(_so_timeout);
            _input = _socket.getInputStream();
            _output = _socket.getOutputStream();
            _open = true;
        } catch (IOException e) {
            // Connection wasn't opened properly
            // so close everything out
            closeSocketAndStreams();
            throw e;
        }
    }

    /**
     * Calling this method indicates that the proxy has successfully created
     * the tunnel to the host. The socket will be switched to the secure socket.
     * Subsequent communication is done via the secure socket. The method can only
     * be called once on a proxied secure connection.
     *
     * @throws IllegalStateException if connection is not secure and proxied or
     * if the socket is already secure.
     * @throws IOException if an error occured creating the secure socket
     */
    public void tunnelCreated()
    throws IllegalStateException, IOException {
        log.trace("enter HttpConnection.tunnelCreated()");

        if (!isSecure() || !isProxied()) {
	    throw new IllegalStateException("Connection must be secure and proxied to use this feature");
	}
        if (_usingSecureSocket) {
	    throw new IllegalStateException("Already using a secure socket");
	}

        if (sslSocketFactory == null) {
            sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        }
        _socket = (new SocketCreator()).createSocket(_socket, _host, _port, true);
        _input = _socket.getInputStream();
        _output = _socket.getOutputStream();
        _usingSecureSocket = true;
        _tunnelEstablished = true;
        log.debug("Secure tunnel created");
    }


    /**
     * Indicates if the connection is completely transparent from end to end.
     *
     * @return true if conncetion is not proxied or tunneled through a transparent
     * proxy; false otherwise.
     */
    public boolean isTransparent() {
        return !isProxied() || _tunnelEstablished;
    }

    /**
     * Return a {@link RequestOutputStream} suitable for writing (possibly 
     * chunked) bytes to my {@link OutputStream}.
     *
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     * @return a stream to write the request to
     */
    public OutputStream getRequestOutputStream()
    throws IOException, IllegalStateException {
        log.trace("enter HttpConnection.getRequestOutputStream()");
        assertOpen();
        return _output;
    }

    /**
     * Return a {@link RequestOutputStream} suitable for writing (possibly 
     * chunked) bytes to my {@link OutputStream}.
     *
     * @param useChunking when <tt>true</tt> the chunked transfer-encoding will
     *      be used
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     * @return a stream to write the request to
     * @deprecated Use new ChunkedOutputStream(httpConnecion.getRequestOutputStream());
     */
    public OutputStream getRequestOutputStream(boolean useChunking) 
        throws IOException, IllegalStateException {
        log.trace("enter HttpConnection.getRequestOutputStream(boolean)");

        assertOpen();
        if (useChunking) {
            return new ChunkedOutputStream(_output);
        } else {
            return _output;
        }
    }

    /**
     * Return a {@link ResponseInputStream} suitable for reading (possibly 
     * chunked) bytes from my {@link InputStream}.
     * <p>
     * If the given {@link HttpMethod} contains
     * a <tt>Transfer-Encoding: chunked</tt> header,
     * the returned stream will be configured
     * to read chunked bytes.
     *
     * @param method This argument is ignored.
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     * @return a stream to read the response from
     * @deprecated Use getResponseInputStream() instead. 
     */
    public InputStream getResponseInputStream(HttpMethod method) 
    throws IOException, IllegalStateException {
        log.trace("enter HttpConnection.getResponseInputStream(HttpMethod)");
        return getResponseInputStream();
    }

    public InputStream getResponseInputStream() 
    throws IOException, IllegalStateException {
        log.trace("enter HttpConnection.getResponseInputStream()");
        assertOpen();
        return _input;
    }

    /**
     * Write the specified bytes to my output stream.
     *
     * @param data the data to be written
     * @throws HttpRecoverableException if a SocketException occurs 
     * @throws IllegalStateException if not connected
     * @throws IOException if an I/O problem occurs
     * @see #write(byte[],int,int) 
     */
    public void write(byte[] data) 
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.write(byte[])");
        this.write(data, 0, data.length);
    }


    /**
     * Write <i>length</i> bytes in <i>data</i> starting at 
     * <i>offset</i> to my output stream. 
     *
     * The general contract for
     * write(b, off, len) is that some of the bytes in the array b are written
     * to the output stream in order; element b[off] is the first byte written
     * and b[off+len-1] is the last byte written by this operation.
     *
     * @param data array containing the data to be written.
     * @param offset the start offset in the data.
     * @param length the number of bytes to write.
     * @throws HttpRecoverableException if a SocketException occurs 
     * @throws IllegalStateException if not connected
     * @throws IOException if an I/O problem occurs
     */
    public void write(byte[] data, int offset, int length) 
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.write(byte[], int, int)");

        if (offset+length > data.length){
            throw new HttpRecoverableException("Unable to write:" +
                    " offset=" + offset +
                    " length=" + length +
                    " data.length=" + data.length);
        }else if (data.length <= 0){
            throw new HttpRecoverableException("Unable to write:" +
                    " data.length=" + data.length);
        }

        assertOpen();

        if(wireLog.isDebugEnabled()) {
            String data_str =  new String(data, offset, length, "ISO-8859-1");
            wireLog.debug(">> \"" + data_str + "\" [\\r\\n]" );
        }
        try {
            _output.write(data, offset, length);
        } catch(SocketException se){
            log.debug("HttpConnection: Socket exception while writing data", se);
            throw new HttpRecoverableException(se.toString());
        } catch(IOException ioe) {
            log.debug("HttpConnection: Exception while writing data", ioe);
            throw ioe;
        }
    }



    /**
     * Write the specified bytes, followed by <tt>"\r\n".getBytes()</tt> to my
     * output stream.
     *
     * @param data the bytes to be written
     * @throws HttpRecoverableException when socket exceptions occur writing data
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     */
    public void writeLine(byte[] data) 
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.writeLine(byte[])");

        assertOpen();
        if(wireLog.isDebugEnabled() && (data.length > 0)) {
	    String data_str =  new String(data);
            wireLog.debug(">> \"" + data_str.trim() + "\" [\\r\\n]" );
        }
        try{
            _output.write(data);
            writeLine();
        } catch(SocketException se){
            log.info("SocketException while writing data to output", se);
            throw new HttpRecoverableException(se.toString());
        } catch(IOException ioe){
            log.info("IOException while writing data to output", ioe);
            throw ioe;
        }
    }

    /**
     * Write <tt>"\r\n".getBytes()</tt> to my output stream.
     *
     * @throws HttpRecoverableException when socket exceptions occur writing
     *      data
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     */
    public void writeLine() 
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.writeLine()");

        wireLog.debug(">> [\\r\\n]");
        try{
            _output.write(CRLF);
        } catch(SocketException se){
            log.warn("HttpConnection: Socket exception while writing data", se);
            throw new HttpRecoverableException(se.toString());
        } catch(IOException ioe){
            log.warn("HttpConnection: IO exception while writing data", ioe);
            throw ioe;
        }
    }

    /**
     * Write the specified String (as bytes) to my output stream.
     *
     * @param data the string to be written
     * @throws HttpRecoverableException when socket exceptions occur writing
     *      data
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     */
    public void print(String data) 
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.print(String)");
        write(data.getBytes());
    }

    /**
     * Write the specified String (as bytes), followed by 
     * <tt>"\r\n".getBytes()</tt> to my output stream.
     *
     * @param data the data to be written
     * @throws HttpRecoverableException when socket exceptions occur writing
     *      data
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     */
    public void printLine(String data)
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.printLine(String)");
        writeLine(data.getBytes());
    }

    /**
     * Write <tt>"\r\n".getBytes()</tt> to my output stream.
     *
     * @throws HttpRecoverableException when socket exceptions occur writing
     *      data
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     */
    public void printLine()
    throws IOException, IllegalStateException, HttpRecoverableException {
        log.trace("enter HttpConnection.printLine()");
        writeLine();
    }

    /**
     * Read up to <tt>"\r\n"</tt> from my (unchunked) input stream.
     *
     * @throws IllegalStateException if I am not connected
     * @throws IOException if an I/O problem occurs
     * @return a line from the response
     */
    public String readLine()
    throws IOException, IllegalStateException {
        log.trace("enter HttpConnection.readLine()");

        assertOpen();
        StringBuffer buf = new StringBuffer();
        for(;;) {
            int ch = _input.read();
            if(ch < 0) {
                if(buf.length() == 0) {
                    return null;
                } else {
                    break;
                }
            } else if (ch == '\r') {
//                log.debug("HttpConnection.readLine() found \\r, continuing");
                continue;
            } else if (ch == '\n') {
//                log.debug("HttpConnection.readLine() found \\n, breaking");
                break;
            }
            buf.append((char)ch);
        }
        if(wireLog.isDebugEnabled() && buf.length() > 0) {
            wireLog.debug("<< \"" + buf.toString() + "\" [\\r\\n]");
        }
        return (buf.toString());
    }

    /**
     * Shutdown my {@link Socket}'s output, via {@link Socket#shutdownOutput}.
     */
    public void shutdownOutput() { 
        log.trace("enter HttpConnection.shutdownOutput()");

        try {
            // Socket.shutdownOutput is a JDK 1.3
            // method. We'll use reflection in case
            // we're running in an older VM
            Class[] paramsClasses = new Class[0];
            Method shutdownOutput = _socket.getClass().getMethod
                ("shutdownOutput", paramsClasses);
            Object[] params = new Object[0];
            shutdownOutput.invoke(_socket, params);
        } catch (Exception ex) {
            log.debug("Unexpected Exception caught", ex);
            // Ignore, and hope everything goes right
        }
        // close output stream?
    }

    /**
     * Close my socket and streams.
     */
    public void close() {
        log.trace("enter HttpConnection.close()");
        closeSocketAndStreams();
    }

    // ------------------------------------------------------ Protected Methods


    /**
     * Close everything out.
     */
    protected void closeSocketAndStreams() {
        log.trace("enter HttpConnection.closeSockedAndStreams()");
        
        if (null != _input) {
	        try {
	            _input.close();
	        } catch(Exception ex) {
		    log.debug("Exception caught when closing input", ex);
	            // ignored
	        }
	        _input = null;
        }

		if (null != _output) {
	        try {
	            _output.close();
	        } catch(Exception ex) {
		    log.debug("Exception caught when closing output", ex);
	            // ignored
	        }
	        _output = null;
		}

		if (null != _socket) {
	        try {
	            _socket.close();
	        } catch(Exception ex) {
		    log.debug("Exception caught when closing socket", ex);
	            // ignored
	        }
	        _socket = null;
		}
        _open = false;
        _tunnelEstablished = false;
        _usingSecureSocket = false;
    }

    /** 
     * Throw an {@link IllegalStateException} if I am connected.
     *
     * @throws IllegalStateException if connected
     */
    protected void assertNotOpen()
    throws IllegalStateException {
        if(_open) {
            throw new IllegalStateException("Connection is open");
        }
    }

    /**
     * Throw an {@link IllegalStateException} if I am not connected.
     *
     * @throws IllegalStateException if not connected
     */
    protected void assertOpen()
    throws IllegalStateException {
        if(!_open) {
            throw new IllegalStateException("Connection is not open");
        }
    }

    // -- javax.net binary isolation

    private class SocketCreator {
        public Socket createSocket(Socket socket, String host, int port, boolean auto) throws IOException {
            return sslSocketFactory.createSocket(socket, host, port, auto);
        }

        public Socket createSocket(String host, int port) throws IOException {
			Socket s = sslSocketFactory.createSocket();
			s.connect(new java.net.InetSocketAddress(host, port), _so_timeout);
            return s;
        }
    }
    // ------------------------------------------------------------- Attributes

    /** Log object for this class. */
    private static final Log log = LogFactory.getLog(HttpConnection.class);
    /** Log for any wire messages. */
    private static final Log wireLog = LogFactory.getLog("httpclient.wire");
    /** My host. */
    private String _host = null;
    /** My port. */
    private int _port = -1;
    /** My proxy host. */
    private String _proxyHost = null;
    /** My proxy port. */
    private int _proxyPort = -1;
    /** My client Socket. */
    private Socket _socket = null;
    /** My InputStream. */
    private InputStream _input = null;
    /** My OutputStream. */
    private OutputStream _output = null;
    /** Whether or not I am connected. */
    private boolean _open = false;
    /** Whether or not I am/should connect via SSL. */
    private boolean _ssl = false;
    /** <tt>"\r\n"</tt>, as bytes. */
    private static final byte[] CRLF = "\r\n".getBytes();
    /** SO_TIMEOUT value */
    private int _so_timeout = 0;
    /** An alternative factory for SSL sockets to use */
    private SSLSocketFactory sslSocketFactory = null;
    /** Whether or not the _socket is a secure one. Note the difference to _ssl */
    private boolean _usingSecureSocket = false;
    /** Whether I am tunneling a proxy or not */
    private boolean _tunnelEstablished = false;
}

