// ========================================================================
// $Id: HttpTunnel.java,v 1.11 2005/10/05 11:14:37 gregwilkins Exp $
// Copyright 2002-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at 
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================

package org.mortbay.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.util.IO;
import org.mortbay.util.LogSupport;

/* ------------------------------------------------------------ */
/** HTTP Tunnel.
 * A HTTP Tunnel can be used to take over a HTTP connection in order to
 * tunnel another protocol over it.  The prime example is the CONNECT method
 * handled by the ProxyHandler to setup a SSL tunnel between the client and
 * the real server.
 *
 * @see HttpConnection
 * @version $Revision: 1.11 $
 * @author Greg Wilkins (gregw)
 */
public class HttpTunnel
{
    private static Log log= LogFactory.getLog(HttpTunnel.class);

    private Thread _thread;
    private int _timeoutMs;
    private Socket _socket;
    private InputStream _sIn;
    private OutputStream _sOut;
    private InputStream _in;
    private OutputStream _out;

    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    protected HttpTunnel()
    {
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param socket The tunnel socket.
     * @param timeoutMs The maximum time to wait for a read on the tunnel. Note that
     * sotimer exceptions are ignored by the tunnel.
     * @param in Alternative input stream or null if using normal socket stream
     * @param out Alternative output stream or null if using normal socket stream
     * @param timeoutMs
     * @throws IOException 
     */
    public HttpTunnel(Socket socket, InputStream in, OutputStream out) throws IOException
    {
        _socket= socket;
        _sIn=in;
        _sOut=out;
        if (_sIn==null)
            _sIn=_socket.getInputStream();
        if (_sOut==null)
            _sOut=socket.getOutputStream();
        _timeoutMs=30000;
    }

    /* ------------------------------------------------------------ */
    /** handle method.
     * This method is called by the HttpConnection.handleNext() method if
     * this HttpTunnel has been set on that connection.
     * The default implementation of this method copies between the HTTP
     * socket and the socket passed in the constructor.
     * @param in 
     * @param out 
     */
    public void handle(InputStream in, OutputStream out)
    {
        Copy copy= new Copy();
        _in= in;
        _out= out;
        try
        {
            _thread= Thread.currentThread();
            copy.start();

            copydata(_sIn, _out);
        }
        catch (Exception e)
        {
            LogSupport.ignore(log, e);
        }
        finally
        {
            try
            {
                _in.close();
                if (_socket!=null)
                {
                    _socket.shutdownOutput();
                    _socket.close();
                }
                else
                {
                    _sIn.close();
                    _sOut.close();
                }
            }
            catch (Exception e)
            {
                LogSupport.ignore(log, e);
            }
            copy.interrupt();
        }
    }

    /* ------------------------------------------------------------ */
    private void copydata(InputStream in, OutputStream out) throws java.io.IOException
    {
        long timestamp= 0;
        long byteCount = 0;
        while (true)
        {
            try
            {
                byteCount = copyBytes(in, out,-1);
                timestamp= 0;
                if (byteCount == -1) {
                    return;
                }
            }
            catch (InterruptedIOException e)
            {
                LogSupport.ignore(log, e);
                if (timestamp == 0)
                    timestamp= System.currentTimeMillis();
                else if (_timeoutMs > 0 && (System.currentTimeMillis() - timestamp) > _timeoutMs)
                    throw e;
            }
        }
    }
    

    /* ------------------------------------------------------------------- */
    /** Copy Stream in to Stream for byteCount bytes or until EOF or exception.
      * @return Copied bytes count or -1 if no bytes were read *and* EOF was reached
    */
    public static int copyBytes(InputStream in,
                                OutputStream out,
                                long byteCount)
         throws IOException
    {     
        byte buffer[] = new byte[IO.bufferSize];
        int len=IO.bufferSize;
        int totalCount=0;
        
        if (byteCount>=0)
        {
            totalCount=(int)byteCount;
            while (byteCount>0)
            {
                try {
                    if (byteCount<IO.bufferSize)
                        len=in.read(buffer,0,(int)byteCount);
                    else
                        len=in.read(buffer,0,IO.bufferSize);
                    if (len==-1 && totalCount==byteCount)
                        totalCount=(int)byteCount - 1;
                } catch (InterruptedIOException e) {
                    if (totalCount==byteCount)
                        throw e;
                    LogSupport.ignore(log, e);
                    len=0;
                }                   
                
                if (len<=0)
                    break;
                
                byteCount -= len;
                out.write(buffer,0,len);
            }
            totalCount -= byteCount;
        }
        else
        {
            while (len>0)
            {
                try {
                    len=in.read(buffer,0,IO.bufferSize);
                    if (len==-1 && totalCount==0)
                        totalCount=-1;
                } catch (InterruptedIOException e) {
                    if (totalCount==0)
                        throw e;
                    LogSupport.ignore(log, e);
                    len=0;
                }
                if (len>0) {
                    out.write(buffer,0,len);
                    totalCount += len;
                }
            }
        }
        return totalCount;
    }
    

    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    /** Copy thread.
     * Helper thread to copy from the HTTP input to the sockets output
     */
    private class Copy extends Thread
    {
        public void run()
        {
            try
            {
                copydata(_in, _sOut);
            }
            catch (Exception e)
            {
                LogSupport.ignore(log, e);
            }
            finally
            {
                try
                {
                    _out.close();
                    if (_socket!=null)
                    {
                        _socket.shutdownInput();
                        _socket.close();
                    }
                    else
                    {
                        _sOut.close();
                        _sIn.close();
                    }
                }
                catch (Exception e)
                {
                    LogSupport.ignore(log, e);
                }
                _thread.interrupt();
            }
        }
    }


    /* ------------------------------------------------------------ */
    /**
     * @return Returns the socket.
     */
    public Socket getSocket()
    {
        return _socket;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return Returns the timeoutMs.
     */
    public int getTimeoutMs()
    {
        return _timeoutMs;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param timeoutMs The timeoutMs to set.
     */
    public void setTimeoutMs(int timeoutMs)
    {
        _timeoutMs = timeoutMs;
    }

}
