/*
 * Copyright (c) 2006-2013 Christian Plattner. All rights reserved.
 * Please refer to the LICENSE.txt for licensing details.
 */

package ch.ethz.ssh2.channel;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ch.ethz.ssh2.ChannelCondition;
import ch.ethz.ssh2.PtySettings;
import ch.ethz.ssh2.ServerConnectionCallback;
import ch.ethz.ssh2.ServerSessionCallback;
import ch.ethz.ssh2.log.Logger;
import ch.ethz.ssh2.packets.*;
import ch.ethz.ssh2.server.ServerConnectionState;
import ch.ethz.ssh2.transport.MessageHandler;
import ch.ethz.ssh2.transport.TransportManager;

/**
 * ChannelManager. Please read the comments in Channel.java.
 * <p/>
 * Besides the crypto part, this is the core of the library.
 *
 * @author Christian Plattner
 * @version $Id$
 */
public class ChannelManager implements MessageHandler
{
	private static final Logger log = Logger.getLogger(ChannelManager.class);

	private final ServerConnectionState server_state;
	private final TransportManager tm;

	private final Map<String, X11ServerData> x11_magic_cookies = new HashMap<String, X11ServerData>();

	private final List<Channel> channels = new ArrayList<Channel>();
	private int nextLocalChannel = 100;
	private boolean shutdown = false;
	private int globalSuccessCounter = 0;
	private int globalFailedCounter = 0;

	private final Map<Integer, RemoteForwardingData> remoteForwardings = new HashMap<Integer, RemoteForwardingData>();

	private final List<IChannelWorkerThread> listenerThreads = new ArrayList<IChannelWorkerThread>();

	private boolean listenerThreadsAllowed = true;

	/**
	 * Constructor for client-mode.
	 * @param tm
	 */
	public ChannelManager(TransportManager tm)
	{
		this.server_state = null;
		this.tm = tm;
		tm.registerMessageHandler(this, 80, 100);
	}

	/**
	 * Constructor for server-mode.
	 * @param state
	 */
	public ChannelManager(ServerConnectionState state)
	{
		this.server_state = state;
		this.tm = state.tm;
		tm.registerMessageHandler(this, 80, 100);
	}

	private Channel getChannel(int id)
	{
		synchronized (channels)
		{
			for (Channel c : channels)
			{
				if (c.localID == id)
					return c;
			}
		}
		return null;
	}

	private void removeChannel(int id)
	{
		synchronized (channels)
		{
			for (Channel c : channels)
			{
				if (c.localID == id)
				{
					channels.remove(c);
					break;
				}
			}
		}
	}

	private int addChannel(Channel c)
	{
		synchronized (channels)
		{
			channels.add(c);
			return nextLocalChannel++;
		}
	}

	private void waitUntilChannelOpen(Channel c) throws IOException
	{
		synchronized (c)
		{
			while (c.state == Channel.STATE_OPENING)
			{
				try
				{
					c.wait();
				}
				catch (InterruptedException e)
				{
					throw new InterruptedIOException(e.getMessage());
				}
			}

			if (c.state != Channel.STATE_OPEN)
			{
				removeChannel(c.localID);

				String detail = c.getReasonClosed();

				if (detail == null)
					detail = "state: " + c.state;

				throw new IOException("Could not open channel (" + detail + ")");
			}
		}
	}

	private void waitForGlobalSuccessOrFailure() throws IOException
	{
        synchronized (channels)
        {
            while ((globalSuccessCounter == 0) && (globalFailedCounter == 0))
            {
                if (shutdown)
                {
                    throw new IOException("The connection is being shutdown");
                }

                try
                {
                    channels.wait();
                }
                catch (InterruptedException e)
                {
                    throw new InterruptedIOException(e.getMessage());
                }
            }
            if ((globalFailedCounter == 0) && (globalSuccessCounter == 1))
            {
                return;
            }
            if ((globalFailedCounter == 1) && (globalSuccessCounter == 0))
            {
                throw new IOException("The server denied the request (did you enable port forwarding?)");
            }
            throw new IOException("Illegal state. The server sent " + globalSuccessCounter
                    + " SSH_MSG_REQUEST_SUCCESS and " + globalFailedCounter + " SSH_MSG_REQUEST_FAILURE messages.");
        }
	}

	private void waitForChannelSuccessOrFailure(Channel c) throws IOException
   	{
   		synchronized (c)
   		{
   			while ((c.successCounter == 0) && (c.failedCounter == 0))
   			{
   				if (c.state != Channel.STATE_OPEN)
   				{
   					String detail = c.getReasonClosed();

   					if (detail == null)
   						detail = "state: " + c.state;

   					throw new IOException("This SSH2 channel is not open (" + detail + ")");
   				}
   				try
   				{
   					c.wait();
   				}
   				catch (InterruptedException ignore)
   				{
   					throw new InterruptedIOException();
   				}
   			}
   			if ((c.failedCounter == 0) && (c.successCounter == 1)) {
                return;
            }
   			if ((c.failedCounter == 1) && (c.successCounter == 0)) {
                throw new IOException("The server denied the request.");
            }
   			throw new IOException("Illegal state. The server sent " + c.successCounter
   					+ " SSH_MSG_CHANNEL_SUCCESS and " + c.failedCounter + " SSH_MSG_CHANNEL_FAILURE messages.");
   		}
   	}

	public void registerX11Cookie(String hexFakeCookie, X11ServerData data)
	{
		synchronized (x11_magic_cookies)
		{
			x11_magic_cookies.put(hexFakeCookie, data);
		}
	}

	public void unRegisterX11Cookie(String hexFakeCookie, boolean killChannels)
	{
		if (hexFakeCookie == null)
			throw new IllegalStateException("hexFakeCookie may not be null");

		synchronized (x11_magic_cookies)
		{
			x11_magic_cookies.remove(hexFakeCookie);
		}

		if (killChannels == false)
			return;

		log.debug("Closing all X11 channels for the given fake cookie");

		List<Channel> channel_copy = new ArrayList<Channel>();

		synchronized (channels)
		{
			channel_copy.addAll(channels);
		}

		for (Channel c : channel_copy)
		{
			synchronized (c)
			{
				if (hexFakeCookie.equals(c.hexX11FakeCookie) == false)
					continue;
			}

			try
			{
				closeChannel(c, "Closing X11 channel since the corresponding session is closing", true);
			}
			catch (IOException ignored)
			{
			}
		}
	}

	public X11ServerData checkX11Cookie(String hexFakeCookie)
	{
		synchronized (x11_magic_cookies)
		{
			if (hexFakeCookie != null)
				return x11_magic_cookies.get(hexFakeCookie);
		}
		return null;
	}

	public void closeAllChannels()
	{
		log.debug("Closing all channels");

		List<Channel> channel_copy = new ArrayList<Channel>();

		synchronized (channels)
		{
			channel_copy.addAll(channels);
		}

		for (Channel c : channel_copy)
		{
			try
			{
				closeChannel(c, "Closing all channels", true);
			}
			catch (IOException ignored)
			{
			}
		}
	}

	public void closeChannel(Channel c, String reason, boolean force) throws IOException
	{
		byte msg[] = new byte[5];

		synchronized (c)
		{
			if (force)
			{
				c.state = Channel.STATE_CLOSED;
				c.EOF = true;
			}

			c.setReasonClosed(reason);

			msg[0] = Packets.SSH_MSG_CHANNEL_CLOSE;
			msg[1] = (byte) (c.remoteID >> 24);
			msg[2] = (byte) (c.remoteID >> 16);
			msg[3] = (byte) (c.remoteID >> 8);
			msg[4] = (byte) (c.remoteID);

			c.notifyAll();
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent == true)
				return;
			tm.sendMessage(msg);
			c.closeMessageSent = true;
		}

		log.debug("Sent SSH_MSG_CHANNEL_CLOSE (channel " + c.localID + ")");
	}

	public void sendEOF(Channel c) throws IOException
	{
		byte[] msg = new byte[5];

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPEN)
				return;

			msg[0] = Packets.SSH_MSG_CHANNEL_EOF;
			msg[1] = (byte) (c.remoteID >> 24);
			msg[2] = (byte) (c.remoteID >> 16);
			msg[3] = (byte) (c.remoteID >> 8);
			msg[4] = (byte) (c.remoteID);
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent == true)
				return;
			tm.sendMessage(msg);
		}


		log.debug("Sent EOF (Channel " + c.localID + "/" + c.remoteID + ")");
	}

	public void sendOpenConfirmation(Channel c) throws IOException
	{
		PacketChannelOpenConfirmation pcoc = null;

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPENING)
				return;

			c.state = Channel.STATE_OPEN;

			pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID, c.localWindow, c.localMaxPacketSize);
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent == true)
				return;
			tm.sendMessage(pcoc.getPayload());
		}
	}

	public void sendData(Channel c, byte[] buffer, int pos, int len) throws IOException
	{
        while (len > 0)
        {
            int thislen = 0;
            byte[] msg;

            synchronized (c)
            {
                while (true)
                {
                    if (c.state == Channel.STATE_CLOSED)
                        throw new ChannelClosedException("SSH channel is closed. (" + c.getReasonClosed() + ")");

                    if (c.state != Channel.STATE_OPEN)
                        throw new ChannelClosedException("SSH channel in strange state. (" + c.state + ")");

                    if (c.remoteWindow != 0)
                        break;

                    try
                    {
                        c.wait();
                    }
                    catch (InterruptedException e)
                    {
                        throw new InterruptedIOException(e.getMessage());
                    }
                }

                /* len > 0, no sign extension can happen when comparing */

                thislen = (c.remoteWindow >= len) ? len : (int) c.remoteWindow;

                int estimatedMaxDataLen = c.remoteMaxPacketSize - (tm.getPacketOverheadEstimate() + 9);

                /* The worst case scenario =) a true bottleneck */

                if (estimatedMaxDataLen <= 0)
                {
                    estimatedMaxDataLen = 1;
                }

                if (thislen > estimatedMaxDataLen)
                    thislen = estimatedMaxDataLen;

                c.remoteWindow -= thislen;

                msg = new byte[1 + 8 + thislen];

                msg[0] = Packets.SSH_MSG_CHANNEL_DATA;
                msg[1] = (byte) (c.remoteID >> 24);
                msg[2] = (byte) (c.remoteID >> 16);
                msg[3] = (byte) (c.remoteID >> 8);
                msg[4] = (byte) (c.remoteID);
                msg[5] = (byte) (thislen >> 24);
                msg[6] = (byte) (thislen >> 16);
                msg[7] = (byte) (thislen >> 8);
                msg[8] = (byte) (thislen);

                System.arraycopy(buffer, pos, msg, 9, thislen);
            }

            synchronized (c.channelSendLock)
            {
                if (c.closeMessageSent == true)
                    throw new ChannelClosedException("SSH channel is closed. (" + c.getReasonClosed() + ")");

                tm.sendMessage(msg);
            }

            pos += thislen;
            len -= thislen;
        }
	}

	public int requestGlobalForward(String bindAddress, int bindPort, String targetAddress, int targetPort)
			throws IOException
	{
		RemoteForwardingData rfd = new RemoteForwardingData();

		rfd.bindAddress = bindAddress;
		rfd.bindPort = bindPort;
		rfd.targetAddress = targetAddress;
		rfd.targetPort = targetPort;

		synchronized (remoteForwardings)
		{
			Integer key = bindPort;

			if (remoteForwardings.get(key) != null)
			{
				throw new IOException("There is already a forwarding for remote port " + bindPort);
			}

			remoteForwardings.put(key, rfd);
		}

		synchronized (channels)
		{
			globalSuccessCounter = globalFailedCounter = 0;
		}

		PacketGlobalForwardRequest pgf = new PacketGlobalForwardRequest(true, bindAddress, bindPort);
		tm.sendMessage(pgf.getPayload());

		log.debug("Requesting a remote forwarding ('" + bindAddress + "', " + bindPort + ")");

		try
		{
			waitForGlobalSuccessOrFailure();
		}
		catch (IOException e)
		{
			synchronized (remoteForwardings)
			{
				remoteForwardings.remove(rfd);
			}
			throw e;
		}

		return bindPort;
	}

	public void requestCancelGlobalForward(int bindPort) throws IOException
	{
		RemoteForwardingData rfd = null;

		synchronized (remoteForwardings)
		{
			rfd = remoteForwardings.get(bindPort);

			if (rfd == null)
				throw new IOException("Sorry, there is no known remote forwarding for remote port " + bindPort);
		}

		synchronized (channels)
		{
			globalSuccessCounter = globalFailedCounter = 0;
		}

		PacketGlobalCancelForwardRequest pgcf = new PacketGlobalCancelForwardRequest(true, rfd.bindAddress,
				rfd.bindPort);
		tm.sendMessage(pgcf.getPayload());

		log.debug("Requesting cancelation of remote forward ('" + rfd.bindAddress + "', " + rfd.bindPort + ")");

		waitForGlobalSuccessOrFailure();

		/* Only now we are sure that no more forwarded connections will arrive */

		synchronized (remoteForwardings)
		{
			remoteForwardings.remove(rfd);
		}
	}

	public void registerThread(IChannelWorkerThread thr) throws IOException
	{
		synchronized (listenerThreads)
		{
			if (listenerThreadsAllowed == false)
				throw new IOException("Too late, this connection is closed.");
			listenerThreads.add(thr);
		}
	}

	public Channel openDirectTCPIPChannel(String host_to_connect, int port_to_connect, String originator_IP_address,
										  int originator_port) throws IOException
	{
		Channel c = new Channel(this);

		synchronized (c)
		{
			c.localID = addChannel(c);
			// end of synchronized block forces writing out to main memory
		}

		PacketOpenDirectTCPIPChannel dtc = new PacketOpenDirectTCPIPChannel(c.localID, c.localWindow,
				c.localMaxPacketSize, host_to_connect, port_to_connect, originator_IP_address, originator_port);

		tm.sendMessage(dtc.getPayload());

		waitUntilChannelOpen(c);

		return c;
	}

	public Channel openSessionChannel() throws IOException
	{
		Channel c = new Channel(this);

		synchronized (c)
		{
			c.localID = addChannel(c);
			// end of synchronized block forces the writing out to main memory
		}

		log.debug("Sending SSH_MSG_CHANNEL_OPEN (Channel " + c.localID + ")");

		PacketOpenSessionChannel smo = new PacketOpenSessionChannel(c.localID, c.localWindow, c.localMaxPacketSize);
		tm.sendMessage(smo.getPayload());

		waitUntilChannelOpen(c);

		return c;
	}

	public void requestPTY(Channel c, String term, int term_width_characters, int term_height_characters,
						   int term_width_pixels, int term_height_pixels, byte[] terminal_modes) throws IOException
	{
		PacketSessionPtyRequest spr;

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")");

			spr = new PacketSessionPtyRequest(c.remoteID, true, term, term_width_characters, term_height_characters,
					term_width_pixels, term_height_pixels, terminal_modes);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent)
				throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")");
			tm.sendMessage(spr.getPayload());
		}

		try
		{
			waitForChannelSuccessOrFailure(c);
		}
		catch (IOException e)
		{
			throw new IOException("PTY request failed", e);
		}
	}

    public void requestWindowChange(Channel c, int term_width_characters, int term_height_characters,
                                    int term_width_pixels, int term_height_pixels) throws IOException {
        PacketWindowChange pwc;

        synchronized (c) {
            if (c.state != Channel.STATE_OPEN)
                throw new IOException("Cannot request window-change on this channel (" + c.getReasonClosed() + ")");

            pwc = new PacketWindowChange(c.remoteID, term_width_characters, term_height_characters,
                    term_width_pixels, term_height_pixels);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized (c.channelSendLock) {
            if (c.closeMessageSent)
                throw new IOException("Cannot request window-change on this channel (" + c.getReasonClosed() + ")");
            tm.sendMessage(pwc.getPayload());
        }

        try
        {
            waitForChannelSuccessOrFailure(c);
        }
        catch (IOException e)
        {
            throw new IOException("The window-change request failed.", e);
        }
    }

    public void requestX11(Channel c, boolean singleConnection, String x11AuthenticationProtocol,
                           String x11AuthenticationCookie, int x11ScreenNumber) throws IOException
	{
		PacketSessionX11Request psr;

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")");

			psr = new PacketSessionX11Request(c.remoteID, true, singleConnection, x11AuthenticationProtocol,
					x11AuthenticationCookie, x11ScreenNumber);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent)
				throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")");
			tm.sendMessage(psr.getPayload());
		}

		log.debug("Requesting X11 forwarding (Channel " + c.localID + "/" + c.remoteID + ")");

		try
        {
			waitForChannelSuccessOrFailure(c);
        }
        catch (IOException e)
		{
			throw new IOException("The X11 request failed.", e);
		}
	}

	public void requestSubSystem(Channel c, String subSystemName) throws IOException
	{
		PacketSessionSubsystemRequest ssr;

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")");

			ssr = new PacketSessionSubsystemRequest(c.remoteID, true, subSystemName);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent)
				throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")");
			tm.sendMessage(ssr.getPayload());
		}

		try
		{
			waitForChannelSuccessOrFailure(c);
		}
		catch (IOException e)
		{
			throw new IOException("The subsystem request failed.", e);
		}
	}

	public void requestExecCommand(Channel c, String cmd) throws IOException
	{
		this.requestExecCommand(c, cmd, null);
	}

	/**
	 * @param charsetName The charset used to convert between Java Unicode Strings and byte encodings
	 */
	public void requestExecCommand(Channel c, String cmd, String charsetName) throws IOException
	{
		PacketSessionExecCommand sm;

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")");

			sm = new PacketSessionExecCommand(c.remoteID, true, cmd);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent)
				throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")");
			tm.sendMessage(sm.getPayload(charsetName));
		}

		log.debug("Executing command (channel " + c.localID + ", '" + cmd + "')");

		try
		{
			waitForChannelSuccessOrFailure(c);
		}
		catch (IOException e)
		{
			throw new IOException("The execute request failed.", e);
		}
	}

	public void requestShell(Channel c) throws IOException
	{
		PacketSessionStartShell sm;

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")");

			sm = new PacketSessionStartShell(c.remoteID, true);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock)
		{
			if (c.closeMessageSent)
				throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")");
			tm.sendMessage(sm.getPayload());
		}

		try
		{
			waitForChannelSuccessOrFailure(c);
		}
		catch (IOException e)
		{
			throw new IOException("The shell request failed.", e);
		}
	}

	public void msgChannelExtendedData(byte[] msg, int msglen) throws IOException
	{
		if (msglen <= 13)
			throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
		int dataType = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);
		int len = ((msg[9] & 0xff) << 24) | ((msg[10] & 0xff) << 16) | ((msg[11] & 0xff) << 8) | (msg[12] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_EXTENDED_DATA message for non-existent channel " + id);

		if (dataType != Packets.SSH_EXTENDED_DATA_STDERR)
			throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has unknown type (" + dataType + ")");

		if (len != (msglen - 13))
			throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong len (calculated " + (msglen - 13)
					+ ", got " + len + ")");

		log.debug("Got SSH_MSG_CHANNEL_EXTENDED_DATA (channel " + id + ", " + len + ")");

		synchronized (c)
		{
			if (c.state == Channel.STATE_CLOSED)
				return; // ignore

			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Got SSH_MSG_CHANNEL_EXTENDED_DATA, but channel is not in correct state ("
						+ c.state + ")");

			if (c.localWindow < len)
				throw new IOException("Remote sent too much data, does not fit into window.");

			c.localWindow -= len;

			System.arraycopy(msg, 13, c.stderrBuffer, c.stderrWritepos, len);
			c.stderrWritepos += len;

			c.notifyAll();
		}
	}

	/**
	 * Wait until for a condition.
	 *
	 * @param c Channel
	 * @param timeout in ms, 0 means no timeout.
	 * @param condition_mask minimum event mask (at least one of the conditions must be fulfilled)
	 * @return all current events
	 */
	public int waitForCondition(Channel c, long timeout, int condition_mask) throws IOException
	{
        long end_time = 0;
        boolean end_time_set = false;

        synchronized (c)
        {
            while (true)
            {
                int current_cond = 0;

                int stdoutAvail = c.stdoutWritepos - c.stdoutReadpos;
                int stderrAvail = c.stderrWritepos - c.stderrReadpos;

                if (stdoutAvail > 0)
                    current_cond = current_cond | ChannelCondition.STDOUT_DATA;

                if (stderrAvail > 0)
                    current_cond = current_cond | ChannelCondition.STDERR_DATA;

                if (c.EOF)
                    current_cond = current_cond | ChannelCondition.EOF;

                if (c.getExitStatus() != null)
                    current_cond = current_cond | ChannelCondition.EXIT_STATUS;

                if (c.getExitSignal() != null)
                    current_cond = current_cond | ChannelCondition.EXIT_SIGNAL;

                if (c.state == Channel.STATE_CLOSED)
                    return current_cond | ChannelCondition.CLOSED | ChannelCondition.EOF;

                if ((current_cond & condition_mask) != 0)
                    return current_cond;

                if (timeout > 0)
                {
                    if (!end_time_set)
                    {
                        end_time = System.currentTimeMillis() + timeout;
                        end_time_set = true;
                    }
                    else
                    {
                        timeout = end_time - System.currentTimeMillis();

                        if (timeout <= 0)
                            return current_cond | ChannelCondition.TIMEOUT;
                    }
                }

                try
                {
                    if (timeout > 0)
                        c.wait(timeout);
                    else
                        c.wait();
                }
                catch (InterruptedException e)
                {
                    throw new InterruptedIOException(e.getMessage());
                }
            }
        }
	}

	public int getAvailable(Channel c, boolean extended) throws IOException
	{
		synchronized (c)
		{
			int avail;

			if (extended)
                avail = c.stderrWritepos - c.stderrReadpos;
			else
                avail = c.stdoutWritepos - c.stdoutReadpos;

			return ((avail > 0) ? avail : (c.EOF ? -1 : 0));
		}
	}

	public int getChannelData(Channel c, boolean extended, byte[] target, int off, int len) throws IOException
	{
        int copylen = 0;
        int increment = 0;
        int remoteID = 0;
        int localID = 0;

        synchronized (c)
        {
            int stdoutAvail = 0;
            int stderrAvail = 0;

            while (true)
            {
                /*
                 * Data available? We have to return remaining data even if the
                 * channel is already closed.
                 */

                stdoutAvail = c.stdoutWritepos - c.stdoutReadpos;
                stderrAvail = c.stderrWritepos - c.stderrReadpos;

                if ((!extended) && (stdoutAvail != 0))
                    break;

                if ((extended) && (stderrAvail != 0))
                    break;

                /* Do not wait if more data will never arrive (EOF or CLOSED) */

                if ((c.EOF) || (c.state != Channel.STATE_OPEN))
                    return -1;

                try
                {
                    c.wait();
                }
                catch (InterruptedException e)
                {
                    throw new InterruptedIOException(e.getMessage());
                }
            }

            /* OK, there is some data. Return it. */

            if (!extended)
            {
                copylen = (stdoutAvail > len) ? len : stdoutAvail;
                System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, target, off, copylen);
                c.stdoutReadpos += copylen;

                if (c.stdoutReadpos != c.stdoutWritepos)

                    System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, c.stdoutBuffer, 0, c.stdoutWritepos
                            - c.stdoutReadpos);

                c.stdoutWritepos -= c.stdoutReadpos;
                c.stdoutReadpos = 0;
            }
            else
            {
                copylen = (stderrAvail > len) ? len : stderrAvail;
                System.arraycopy(c.stderrBuffer, c.stderrReadpos, target, off, copylen);
                c.stderrReadpos += copylen;

                if (c.stderrReadpos != c.stderrWritepos)

                    System.arraycopy(c.stderrBuffer, c.stderrReadpos, c.stderrBuffer, 0, c.stderrWritepos
                            - c.stderrReadpos);

                c.stderrWritepos -= c.stderrReadpos;
                c.stderrReadpos = 0;
            }

            if (c.state != Channel.STATE_OPEN)
                return copylen;

            if (c.localWindow < ((Channel.CHANNEL_BUFFER_SIZE + 1) / 2))
            {
                int minFreeSpace = Math.min(Channel.CHANNEL_BUFFER_SIZE - c.stdoutWritepos,
                        Channel.CHANNEL_BUFFER_SIZE - c.stderrWritepos);

                increment = minFreeSpace - c.localWindow;
                c.localWindow = minFreeSpace;
            }

            remoteID = c.remoteID; /* read while holding the lock */
            localID = c.localID; /* read while holding the lock */
        }

        /*
         * If a consumer reads stdout and stdin in parallel, we may end up with
         * sending two msgWindowAdjust messages. Luckily, it
         * does not matter in which order they arrive at the server.
         */

        if (increment > 0)
        {
            log.debug("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + localID + ", " + increment + ")");

            synchronized (c.channelSendLock)
            {
                byte[] msg = c.msgWindowAdjust;

                msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST;
                msg[1] = (byte) (remoteID >> 24);
                msg[2] = (byte) (remoteID >> 16);
                msg[3] = (byte) (remoteID >> 8);
                msg[4] = (byte) (remoteID);
                msg[5] = (byte) (increment >> 24);
                msg[6] = (byte) (increment >> 16);
                msg[7] = (byte) (increment >> 8);
                msg[8] = (byte) (increment);

                if (c.closeMessageSent == false)
                    tm.sendMessage(msg);
            }
        }

        return copylen;
	}

	public void msgChannelData(byte[] msg, int msglen) throws IOException
	{
		if (msglen <= 9)
			throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
		int len = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_DATA message for non-existent channel " + id);

		if (len != (msglen - 9))
			throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong len (calculated " + (msglen - 9) + ", got "
					+ len + ")");

		log.debug("Got SSH_MSG_CHANNEL_DATA (channel " + id + ", " + len + ")");

		synchronized (c)
		{
			if (c.state == Channel.STATE_CLOSED)
				return; // ignore

			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Got SSH_MSG_CHANNEL_DATA, but channel is not in correct state (" + c.state + ")");

			if (c.localWindow < len)
				throw new IOException("Remote sent too much data, does not fit into window.");

			c.localWindow -= len;

			System.arraycopy(msg, 9, c.stdoutBuffer, c.stdoutWritepos, len);
			c.stdoutWritepos += len;

			c.notifyAll();
		}
	}

	public void msgChannelWindowAdjust(byte[] msg, int msglen) throws IOException
	{
		if (msglen != 9)
			throw new IOException("SSH_MSG_CHANNEL_WINDOW_ADJUST message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
		int windowChange = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_WINDOW_ADJUST message for non-existent channel " + id);

		synchronized (c)
		{
			final long huge = 0xFFFFffffL; /* 2^32 - 1 */

			c.remoteWindow += (windowChange & huge); /* avoid sign extension */

			/* TODO - is this a good heuristic? */

			if ((c.remoteWindow > huge))
				c.remoteWindow = huge;

			c.notifyAll();
		}


		log.debug("Got SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + id + ", " + windowChange + ")");
	}

	public void msgChannelOpen(byte[] msg, int msglen) throws IOException
	{
		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		String channelType = tr.readString();
		int remoteID = tr.readUINT32(); /* sender channel */
		int remoteWindow = tr.readUINT32(); /* initial window size */
		int remoteMaxPacketSize = tr.readUINT32(); /* maximum packet size */

		if ("x11".equals(channelType))
		{
			synchronized (x11_magic_cookies)
			{
				/* If we did not request X11 forwarding, then simply ignore this bogus request. */

				if (x11_magic_cookies.size() == 0)
				{
					PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID,
							Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "X11 forwarding not activated", "");

					tm.sendAsynchronousMessage(pcof.getPayload());

					log.warning("Unexpected X11 request, denying it!");

					return;
				}
			}

			String remoteOriginatorAddress = tr.readString();
			int remoteOriginatorPort = tr.readUINT32();

			Channel c = new Channel(this);

			synchronized (c)
			{
				c.remoteID = remoteID;
				c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */
				c.remoteMaxPacketSize = remoteMaxPacketSize;
				c.localID = addChannel(c);
			}

			/*
			 * The open confirmation message will be sent from another thread
			 */

			RemoteX11AcceptThread rxat = new RemoteX11AcceptThread(c, remoteOriginatorAddress, remoteOriginatorPort);
			rxat.setDaemon(true);
			rxat.start();

			return;
		}

		if ("forwarded-tcpip".equals(channelType))
		{
			String remoteConnectedAddress = tr.readString(); /* address that was connected */
			int remoteConnectedPort = tr.readUINT32(); /* port that was connected */
			String remoteOriginatorAddress = tr.readString(); /* originator IP address */
			int remoteOriginatorPort = tr.readUINT32(); /* originator port */

			RemoteForwardingData rfd = null;

			synchronized (remoteForwardings)
			{
				rfd = remoteForwardings.get(remoteConnectedPort);
			}

			if (rfd == null)
			{
				PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID,
						Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
						"No thanks, unknown port in forwarded-tcpip request", "");

				/* Always try to be polite. */

				tm.sendAsynchronousMessage(pcof.getPayload());

				log.debug("Unexpected forwarded-tcpip request, denying it!");

				return;
			}

			Channel c = new Channel(this);

			synchronized (c)
			{
				c.remoteID = remoteID;
				c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */
				c.remoteMaxPacketSize = remoteMaxPacketSize;
				c.localID = addChannel(c);
			}

			/*
			 * The open confirmation message will be sent from another thread.
			 */

			RemoteAcceptThread rat = new RemoteAcceptThread(c, remoteConnectedAddress, remoteConnectedPort,
					remoteOriginatorAddress, remoteOriginatorPort, rfd.targetAddress, rfd.targetPort);

			rat.setDaemon(true);
			rat.start();

			return;
		}

		if ((server_state != null) && ("session".equals(channelType)))
		{
			ServerConnectionCallback cb = null;
			
			synchronized (server_state)
			{
				cb = server_state.cb_conn;
			}
			
			if (cb == null)
			{
				tm.sendAsynchronousMessage(new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
						"Sessions are currently not enabled", "en").getPayload());
				
				return;
			}
			
			final Channel c = new Channel(this);

			synchronized (c)
			{
				c.remoteID = remoteID;
				c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */
				c.remoteMaxPacketSize = remoteMaxPacketSize;
				c.localID = addChannel(c);
				c.state = Channel.STATE_OPEN;
				c.ss = new ServerSessionImpl(c);
			}

			PacketChannelOpenConfirmation pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID,
					c.localWindow, c.localMaxPacketSize);

			tm.sendAsynchronousMessage(pcoc.getPayload());

			c.ss.sscb = cb.acceptSession(c.ss);

			return;
		}

		/* Tell the server that we have no idea what it is talking about */

		PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE,
				"Unknown channel type", "");

		tm.sendAsynchronousMessage(pcof.getPayload());


		log.warning("The peer tried to open an unsupported channel type (" + channelType + ")");
	}

	/* Starts the given runnable in a foreground (non-daemon) thread */
	private void runAsync(Runnable r)
	{
		Thread t = new Thread(r);
		t.start();		
	}
	
	public void msgChannelRequest(byte[] msg, int msglen) throws IOException
	{
		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		int id = tr.readUINT32();

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_REQUEST message for non-existent channel " + id);

		ServerSessionImpl server_session = null;

		if (server_state != null)
		{
			synchronized (c)
			{
				server_session = c.ss;
			}
		}

		String type = tr.readString("US-ASCII");
		boolean wantReply = tr.readBoolean();

		log.debug("Got SSH_MSG_CHANNEL_REQUEST (channel " + id + ", '" + type + "')");

		if (type.equals("exit-status"))
		{
			if (wantReply != false)
				throw new IOException(
						"Badly formatted SSH_MSG_CHANNEL_REQUEST exit-status message, 'want reply' is true");

			int exit_status = tr.readUINT32();

			if (tr.remain() != 0)
				throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");

			synchronized (c)
			{
				c.exit_status = exit_status;
				c.notifyAll();
			}

			log.debug("Got EXIT STATUS (channel " + id + ", status " + exit_status + ")");

			return;
		}

		if ((server_state == null) && (type.equals("exit-signal")))
		{
			if (wantReply != false)
				throw new IOException(
						"Badly formatted SSH_MSG_CHANNEL_REQUEST exit-signal message, 'want reply' is true");

			String signame = tr.readString("US-ASCII");
			tr.readBoolean();
			tr.readString();
			tr.readString();

			if (tr.remain() != 0)
				throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");

			synchronized (c)
			{
				c.exit_signal = signame;
				c.notifyAll();
			}

			log.debug("Got EXIT SIGNAL (channel " + id + ", signal " + signame + ")");

			return;
		}

		if ((server_session != null) && (type.equals("pty-req")))
		{
			PtySettings pty = new PtySettings();

			pty.term = tr.readString();
			pty.term_width_characters = tr.readUINT32();
			pty.term_height_characters = tr.readUINT32();
			pty.term_width_pixels = tr.readUINT32();
			pty.term_height_pixels = tr.readUINT32();
			pty.terminal_modes = tr.readByteString();

			if (tr.remain() != 0)
				throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
			
			Runnable run_after_sending_success = null;
			
			ServerSessionCallback sscb = server_session.getServerSessionCallback();

			if (sscb != null)
				run_after_sending_success = sscb.requestPtyReq(server_session, pty);

			if (wantReply)
			{
				if (run_after_sending_success != null)
				{
					tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload());
				}
				else
				{
					tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
				}			
			}
			
			if (run_after_sending_success != null)
			{
				runAsync(run_after_sending_success);
			}
			
			return;
		}

		if ((server_session != null) && (type.equals("shell")))
		{
			if (tr.remain() != 0)
				throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
			
			Runnable run_after_sending_success = null;
			ServerSessionCallback sscb = server_session.getServerSessionCallback();

			if (sscb != null)
				run_after_sending_success = sscb.requestShell(server_session);

			if (wantReply)
			{
				if (run_after_sending_success != null)
				{
					tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload());
				}
				else
				{
					tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
				}
			}
			
			if (run_after_sending_success != null)
			{
				runAsync(run_after_sending_success);
			}
			
			return;
		}
		
		if ((server_session != null) && (type.equals("exec")))
		{
			String command = tr.readString();
			
			if (tr.remain() != 0)
				throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
			
			Runnable run_after_sending_success = null;
			ServerSessionCallback sscb = server_session.getServerSessionCallback();

			if (sscb != null)
				run_after_sending_success = sscb.requestExec(server_session, command);

			if (wantReply)
			{
				if (run_after_sending_success != null)
				{
					tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload());
				}
				else
				{
					tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
				}
			}
			
			if (run_after_sending_success != null)
			{
				runAsync(run_after_sending_success);
			}
			
			return;
		}

		/* We simply ignore unknown channel requests, however, if the server wants a reply,
		 * then we signal that we have no idea what it is about.
		 */

		if (wantReply)
		{
			tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
		}

		log.debug("Channel request '" + type + "' is not known, ignoring it");
	}

	public void msgChannelEOF(byte[] msg, int msglen) throws IOException
	{
		if (msglen != 5)
			throw new IOException("SSH_MSG_CHANNEL_EOF message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_EOF message for non-existent channel " + id);

		synchronized (c)
		{
			c.EOF = true;
			c.notifyAll();
		}

		log.debug("Got SSH_MSG_CHANNEL_EOF (channel " + id + ")");
	}

	public void msgChannelClose(byte[] msg, int msglen) throws IOException
	{
		if (msglen != 5)
			throw new IOException("SSH_MSG_CHANNEL_CLOSE message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_CLOSE message for non-existent channel " + id);

		synchronized (c)
		{
			c.EOF = true;
			c.state = Channel.STATE_CLOSED;
			c.setReasonClosed("Close requested by remote");
			c.closeMessageRecv = true;

			removeChannel(c.localID);

			c.notifyAll();
		}

		log.debug("Got SSH_MSG_CHANNEL_CLOSE (channel " + id + ")");
	}

	public void msgChannelSuccess(byte[] msg, int msglen) throws IOException
	{
		if (msglen != 5)
			throw new IOException("SSH_MSG_CHANNEL_SUCCESS message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_SUCCESS message for non-existent channel " + id);

		synchronized (c)
		{
			c.successCounter++;
			c.notifyAll();
		}

		log.debug("Got SSH_MSG_CHANNEL_SUCCESS (channel " + id + ")");
	}

	public void msgChannelFailure(byte[] msg, int msglen) throws IOException
	{
		if (msglen != 5)
			throw new IOException("SSH_MSG_CHANNEL_FAILURE message has wrong size (" + msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_FAILURE message for non-existent channel " + id);

		synchronized (c)
		{
			c.failedCounter++;
			c.notifyAll();
		}

		log.debug("Got SSH_MSG_CHANNEL_FAILURE (channel " + id + ")");
	}

	public void msgChannelOpenConfirmation(byte[] msg, int msglen) throws IOException
	{
		PacketChannelOpenConfirmation sm = new PacketChannelOpenConfirmation(msg, 0, msglen);

		Channel c = getChannel(sm.recipientChannelID);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for non-existent channel "
					+ sm.recipientChannelID);

		synchronized (c)
		{
			if (c.state != Channel.STATE_OPENING)
				throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for channel "
						+ sm.recipientChannelID);

			c.remoteID = sm.senderChannelID;
			c.remoteWindow = sm.initialWindowSize & 0xFFFFffffL; /* convert UINT32 to long */
			c.remoteMaxPacketSize = sm.maxPacketSize;
			c.state = Channel.STATE_OPEN;
			c.notifyAll();
		}

		log.debug("Got SSH_MSG_CHANNEL_OPEN_CONFIRMATION (channel " + sm.recipientChannelID + " / remote: "
				+ sm.senderChannelID + ")");
	}

	public void msgChannelOpenFailure(byte[] msg, int msglen) throws IOException
	{
		if (msglen < 5)
			throw new IOException("SSH_MSG_CHANNEL_OPEN_FAILURE message has wrong size (" + msglen + ")");

		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		int id = tr.readUINT32(); /* sender channel */

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE message for non-existent channel " + id);

		int reasonCode = tr.readUINT32();
		String description = tr.readString("UTF-8");

		String reasonCodeSymbolicName = null;

		switch (reasonCode)
		{
			case 1:
				reasonCodeSymbolicName = "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED";
				break;
			case 2:
				reasonCodeSymbolicName = "SSH_OPEN_CONNECT_FAILED";
				break;
			case 3:
				reasonCodeSymbolicName = "SSH_OPEN_UNKNOWN_CHANNEL_TYPE";
				break;
			case 4:
				reasonCodeSymbolicName = "SSH_OPEN_RESOURCE_SHORTAGE";
				break;
			default:
				reasonCodeSymbolicName = "UNKNOWN REASON CODE (" + reasonCode + ")";
		}

		StringBuilder descriptionBuffer = new StringBuilder();
		descriptionBuffer.append(description);

		for (int i = 0; i < descriptionBuffer.length(); i++)
		{
			char cc = descriptionBuffer.charAt(i);

			if ((cc >= 32) && (cc <= 126))
				continue;
			descriptionBuffer.setCharAt(i, '\uFFFD');
		}

		synchronized (c)
		{
			c.EOF = true;
			c.state = Channel.STATE_CLOSED;
			c.setReasonClosed("The server refused to open the channel (" + reasonCodeSymbolicName + ", '"
					+ descriptionBuffer.toString() + "')");
			c.notifyAll();
		}

		log.debug("Got SSH_MSG_CHANNEL_OPEN_FAILURE (channel " + id + ")");
	}

	public void msgGlobalRequest(byte[] msg, int msglen) throws IOException
	{
		/* Currently we do not support any kind of global request */

		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		String requestName = tr.readString();
		boolean wantReply = tr.readBoolean();

		if (wantReply)
		{
			byte[] reply_failure = new byte[1];
			reply_failure[0] = Packets.SSH_MSG_REQUEST_FAILURE;

			tm.sendAsynchronousMessage(reply_failure);
		}

		/* We do not clean up the requestName String - that is OK for debug */

		log.debug("Got SSH_MSG_GLOBAL_REQUEST (" + requestName + ")");
	}

	public void msgGlobalSuccess() throws IOException
	{
		synchronized (channels)
		{
			globalSuccessCounter++;
			channels.notifyAll();
		}

		log.debug("Got SSH_MSG_REQUEST_SUCCESS");
	}

	public void msgGlobalFailure() throws IOException
	{
		synchronized (channels)
		{
			globalFailedCounter++;
			channels.notifyAll();
		}

		log.debug("Got SSH_MSG_REQUEST_FAILURE");
	}

	public void handleMessage(byte[] msg, int msglen) throws IOException
	{
		if (msg == null)
		{

			log.debug("HandleMessage: got shutdown");

			synchronized (listenerThreads)
			{
				for (IChannelWorkerThread lat : listenerThreads)
				{
					lat.stopWorking();
				}
				listenerThreadsAllowed = false;
			}

			synchronized (channels)
			{
				shutdown = true;

				for (Channel c : channels)
				{
					synchronized (c)
					{
						c.EOF = true;
						c.state = Channel.STATE_CLOSED;
						c.setReasonClosed("The connection is being shutdown");
						c.closeMessageRecv = true; /*
													* You never know, perhaps
													* we are waiting for a
													* pending close message
													* from the server...
													*/
						c.notifyAll();
					}
				}

				channels.clear();
				channels.notifyAll(); /* Notify global response waiters */
				return;
			}
		}

		switch (msg[0])
		{
			case Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
				msgChannelOpenConfirmation(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST:
				msgChannelWindowAdjust(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_DATA:
				msgChannelData(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_EXTENDED_DATA:
				msgChannelExtendedData(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_REQUEST:
				msgChannelRequest(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_EOF:
				msgChannelEOF(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_OPEN:
				msgChannelOpen(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_CLOSE:
				msgChannelClose(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_SUCCESS:
				msgChannelSuccess(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_FAILURE:
				msgChannelFailure(msg, msglen);
				break;
			case Packets.SSH_MSG_CHANNEL_OPEN_FAILURE:
				msgChannelOpenFailure(msg, msglen);
				break;
			case Packets.SSH_MSG_GLOBAL_REQUEST:
				msgGlobalRequest(msg, msglen);
				break;
			case Packets.SSH_MSG_REQUEST_SUCCESS:
				msgGlobalSuccess();
				break;
			case Packets.SSH_MSG_REQUEST_FAILURE:
				msgGlobalFailure();
				break;
			default:
				throw new IOException("Cannot handle unknown channel message " + (msg[0] & 0xff));
		}
	}
}
