/**
 * Copyright 2003-2016 SSHTOOLS Limited. All Rights Reserved.
 *
 * For product documentation visit https://www.sshtools.com/
 *
 * This file is part of J2SSH Maverick.
 *
 * J2SSH Maverick is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * J2SSH Maverick 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with J2SSH Maverick.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sshtools.ssh2;

import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;

import com.sshtools.logging.Log;
import com.sshtools.ssh.ChannelEventListener;
import com.sshtools.ssh.ChannelOpenException;
import com.sshtools.ssh.ForwardingRequestListener;
import com.sshtools.ssh.PasswordAuthentication;
import com.sshtools.ssh.PublicKeyAuthentication;
import com.sshtools.ssh.SshAuthentication;
import com.sshtools.ssh.SshClient;
import com.sshtools.ssh.SshConnector;
import com.sshtools.ssh.SshContext;
import com.sshtools.ssh.SshException;
import com.sshtools.ssh.SshSession;
import com.sshtools.ssh.SshTransport;
import com.sshtools.ssh.SshTunnel;
import com.sshtools.ssh.components.SshKeyExchangeClient;
import com.sshtools.ssh.message.SshAbstractChannel;
import com.sshtools.util.ByteArrayReader;
import com.sshtools.util.ByteArrayWriter;

/**
 * <p>
 * Implementation of an <a href="../ssh/SshClient.html">SshClient</a> for the
 * SSH2 protocol; this provides the ability to create custom channels and
 * sending/receiving of global requests in addition to the standard <a
 * href="../ssh/SshClient.html">SshClient</a> contract.
 * </p>
 * 
 * @author Lee David Painter
 */
public class Ssh2Client implements SshClient {

	TransportProtocol transport;
	SshTransport io;
	AuthenticationProtocol authentication;
	ConnectionProtocol connection;
	String localIdentification;
	String remoteIdentification;
	String[] authenticationMethods;
	String username;
	Hashtable<String, ForwardingRequestListener> forwardingListeners = new Hashtable<String, ForwardingRequestListener>();
	Hashtable<String, String> forwardingDestinations = new Hashtable<String, String>();
	ForwardingRequestChannelFactory requestFactory = new ForwardingRequestChannelFactory();
	SshAuthentication auth;
	SshConnector connector;
	boolean isXForwarding = false;
	boolean buffered;

	/**
	 * Default constructor called by <a
	 * href="../ssh/SshConnector.html">SshConnector</a>.
	 * 
	 */
	public Ssh2Client() {
	}

	public void connect(SshTransport io, SshContext context,
			SshConnector connector, String username,
			String localIdentification, String remoteIdentification,
			boolean buffered) throws SshException {

		this.io = io;
		this.localIdentification = localIdentification;
		this.remoteIdentification = remoteIdentification;
		this.username = username;
		this.buffered = buffered;
		this.connector = connector;

		if (username == null) {
			try {
				io.close();
			} catch (IOException ex) {
				if (Log.isDebugEnabled()) {
					Log.debug(
							this,
							"RECIEVED IOException IN Ssh2Client.connect:"
									+ ex.getMessage());
				}
			}
			throw new SshException("You must supply a valid username!",
					SshException.BAD_API_USAGE);
		}

		if (!(context instanceof Ssh2Context)) {
			try {
				io.close();
			} catch (IOException ex) {
				if (Log.isDebugEnabled()) {
					Log.debug(
							this,
							"RECIEVED IOException IN Ssh2Client.connect:"
									+ ex.getMessage());
				}

			}
			throw new SshException("Ssh2Context required!",
					SshException.BAD_API_USAGE);
		}

		if (Log.isDebugEnabled()) {
			Log.debug(
					this,
					"Connecting " + username + "@" + io.getHost() + ":"
							+ io.getPort());
			Log.debug(this, "Remote identification is "
					+ remoteIdentification);
		}

		transport = new TransportProtocol();

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Starting transport protocol");
		}

		transport.startTransportProtocol(io, (Ssh2Context) context,
				localIdentification, remoteIdentification, this);

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Starting authentication protocol");
		}

		authentication = new AuthenticationProtocol(transport);
		authentication.setBannerDisplay(((Ssh2Context) context)
				.getBannerDisplay());

		connection = new ConnectionProtocol(this.transport, context, buffered);
		connection.addChannelFactory(requestFactory);

		getAuthenticationMethods(username);

		if (Log.isDebugEnabled()) {
			Log.debug(this, "SSH connection established");
		}
	}

	/**
	 * Get a list of authentication methods for the user.
	 * 
	 * @param username
	 *            the name of the user
	 * @return an array of authentication methods, for example { "password",
	 *         "publickey" }
	 * @throws SshException
	 */
	public String[] getAuthenticationMethods(String username)
			throws SshException {
		verifyConnection(false);

		if (authenticationMethods == null) {

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Requesting authentication methods");
			}

			String methods = authentication.getAuthenticationMethods(username,
					ConnectionProtocol.SERVICE_NAME);

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Available authentications are "
						+ methods);
			}
			Vector<String> tmp = new Vector<String>();
			int idx;
			while (methods != null) {
				idx = methods.indexOf(',');
				if (idx > -1) {
					tmp.addElement(methods.substring(0, idx));
					methods = methods.substring(idx + 1);
				} else {
					tmp.addElement(methods);
					methods = null;
				}
			}
			authenticationMethods = new String[tmp.size()];
			tmp.copyInto(authenticationMethods);

			/*
			 * if there are no authentication methods, then check if
			 * isAuthenticated if isAuthenticated then need to start the message
			 * pump, as authenticate will not be called
			 */
			if (isAuthenticated()) {
				connection.start();
			}
		}

		return authenticationMethods;
	}

	/**
	 * this method is called if a user attempts password authentication it
	 * determines whether password authentication is possible. if it isnt, but
	 * keyboard interactive is possible, it authenticates using that instead
	 */
	private SshAuthentication checkForPasswordOverKBI(SshAuthentication auth) {
		boolean kbiAuthenticationPossible = false;
		for (int i = 0; i < authenticationMethods.length; i++) {
			if (authenticationMethods[i].equals("password")) {
				// password authentication is possible so return auth unchanged
				return auth;
			}
			if ((authenticationMethods[i].equals("keyboard-interactive"))) {
				// if none of the subsequent methods are password then have
				// option to use kbi instead
				kbiAuthenticationPossible = true;
			}
		}
		// password is not possible, so attempt kbi
		if (kbiAuthenticationPossible) {
			// create KBIAuthentication instance
			KBIAuthentication kbi = new KBIAuthentication();
			// set the username that the user entered
			kbi.setUsername(((PasswordAuthentication) auth).getUsername());

			// set request handler, that sets the password the user entered as
			// response to any prompts
			kbi.setKBIRequestHandler(new KBIRequestHandlerWhenUserUsingPasswordAuthentication(
					(PasswordAuthentication) auth));

			return kbi;
		}
		// neither password nor kbi is possible so return auth unchanged so that
		// the normal error message is returned
		return auth;
	}

	/**
	 * <p>
	 * Request handler that sets the password the user entered as response to
	 * any prompts
	 * </p>
	 * 
	 * @author David Hodgins
	 */
	private static class KBIRequestHandlerWhenUserUsingPasswordAuthentication
			implements KBIRequestHandler {
		private String password;

		public KBIRequestHandlerWhenUserUsingPasswordAuthentication(
				PasswordAuthentication pwdAuth) {
			password = pwdAuth.getPassword();
		}

		/**
		 * Called by the <em>keyboard-interactive</em> authentication mechanism
		 * when the server requests information from the user. Each prompt
		 * should be displayed to the user with their response recorded within
		 * the prompt object.
		 * 
		 * @param name
		 * @param instruction
		 * @param prompts
		 */
		public boolean showPrompts(String name, String instruction,
				KBIPrompt[] prompts) {
			for (int i = 0; i < prompts.length; i++) {
				prompts[i].setResponse(password);
			}
			return true;
		}

	}

	public int authenticate(SshAuthentication auth) throws SshException {
		verifyConnection(false);

		if (isAuthenticated())
			throw new SshException(
					"User is already authenticated! Did you check isAuthenticated?",
					SshException.BAD_API_USAGE);

		if (auth.getUsername() == null) {
			auth.setUsername(username);
		}

		if (auth instanceof PasswordAuthentication
				|| auth instanceof Ssh2PasswordAuthentication) {
			auth = checkForPasswordOverKBI(auth);
		}

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Authenticating with " + auth.getMethod());
		}

		int result;

		if (auth instanceof PasswordAuthentication
				&& !(auth instanceof Ssh2PasswordAuthentication)) {
			// We need to create an instance of Ssh2PasswordAuthentication
			Ssh2PasswordAuthentication pwd = new Ssh2PasswordAuthentication();
			pwd.setUsername(((PasswordAuthentication) auth).getUsername());
			pwd.setPassword(((PasswordAuthentication) auth).getPassword());

			result = authentication.authenticate(pwd,
					ConnectionProtocol.SERVICE_NAME);

			if (pwd.requiresPasswordChange()) {
				disconnect();
				throw new SshException("Password change required!",
						SshException.CANCELLED_CONNECTION);
			}
		} else if (auth instanceof PublicKeyAuthentication
				&& !(auth instanceof Ssh2PublicKeyAuthentication)) {
			// We need to create an Ssh2PublicKeyAuthentication object
			Ssh2PublicKeyAuthentication pk = new Ssh2PublicKeyAuthentication();
			pk.setUsername(((PublicKeyAuthentication) auth).getUsername());
			pk.setPublicKey(((PublicKeyAuthentication) auth).getPublicKey());
			pk.setPrivateKey(((PublicKeyAuthentication) auth).getPrivateKey());

			result = authentication.authenticate(pk,
					ConnectionProtocol.SERVICE_NAME);

		} else if (auth instanceof AuthenticationClient) {
			// Execute an AuthenticationClient instance
			result = authentication.authenticate((AuthenticationClient) auth,
					ConnectionProtocol.SERVICE_NAME);
		} else {
			throw new SshException("Invalid authentication client",
					SshException.BAD_API_USAGE);
		}

		if (result == SshAuthentication.COMPLETE) {
			this.auth = auth;
			connection.start();
		}

		if (Log.isDebugEnabled()) {
			switch (result) {
			case SshAuthentication.COMPLETE:
				Log.debug(this, "Authentication complete");
				break;
			case SshAuthentication.FAILED:
				Log.debug(this, "Authentication failed");
				break;
			case SshAuthentication.FURTHER_AUTHENTICATION_REQUIRED:
				Log.debug(this,
						"Authentication successful but further authentication required");
				break;
			case SshAuthentication.CANCELLED:
				Log.debug(this, "Authentication cancelled");
				break;
			case SshAuthentication.PUBLIC_KEY_ACCEPTABLE:
				Log.debug(this,
						"Server accepts the public key provided");
				break;
			default:
				Log.debug(this, "Unknown authentication result "
						+ result);
				break;
			}
		}

		return result;
	}

	public boolean isAuthenticated() {
		return authentication.isAuthenticated();
	}

	public void disconnect() {

		try {

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Disconnecting");
			}
			connection.signalClosingState();
			connection.stop();
			transport.disconnect(TransportProtocol.BY_APPLICATION,
					"The user disconnected the application");
		} catch (Throwable t) {
		}

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Disconnected");
		}
	}

	public void exit() {

		try {

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Disconnecting");
			}
			connection.signalClosingState();
			transport.disconnect(TransportProtocol.BY_APPLICATION,
					"The user disconnected the application");
		} catch (Throwable t) {
		}

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Disconnected");
		}
	}

	public boolean isConnected() {
		return transport.isConnected();
	}

	/**
	 * The SSH transport protocol exchanges keys at the beginning of the
	 * session; the specification recommends that these keys be re-exchanged
	 * after each gigabyte of transmitted data or after each hour of connection
	 * time, whichever comes sooner. This method can be called at anytime to
	 * begin the key exchange process.
	 * 
	 * @throws SshException
	 */
	public void forceKeyExchange() throws SshException {
		if (Log.isDebugEnabled()) {
			Log.debug(this, "Forcing key exchange");
		}
		transport.sendKeyExchangeInit(false);
	}

	public SshSession openSessionChannel() throws SshException,
			ChannelOpenException {
		return openSessionChannel(32768, 32768, null);
	}

	public SshSession openSessionChannel(long timeout) throws SshException,
			ChannelOpenException {
		return openSessionChannel(32768, 32768, null, timeout);
	}

	public SshSession openSessionChannel(ChannelEventListener listener,
			long timeout) throws SshException, ChannelOpenException {
		return openSessionChannel(32768, 32768, listener, timeout);
	}

	public SshSession openSessionChannel(ChannelEventListener listener)
			throws SshException, ChannelOpenException {
		return openSessionChannel(32768, 32768, listener);
	}

	/**
	 * Additional method to open a session with SSH2 specific features.
	 * 
	 * @param windowspace
	 *            the initial amount of window space available
	 * @param packetsize
	 *            the maximum packet size
	 * @param listener
	 *            an event listener to add before opening
	 * @return an open session
	 * @throws SshException
	 * @throws ChannelOpenException
	 */
	public Ssh2Session openSessionChannel(int windowspace, int packetsize,
			ChannelEventListener listener) throws ChannelOpenException,
			SshException {
		return openSessionChannel(windowspace, packetsize, listener, 0);
	}

	public Ssh2Session openSessionChannel(int windowspace, int packetsize,
			ChannelEventListener listener, long timeout)
			throws ChannelOpenException, SshException {
		verifyConnection(true);

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Opening session channel windowspace="
					+ windowspace + " packetsize=" + packetsize);
		}

		Ssh2Session channel = new Ssh2Session(windowspace, packetsize, this);
		if (listener != null) {
			channel.addChannelEventListener(listener);
		}

		connection.openChannel(channel, null, timeout);

		if (Log.isDebugEnabled()) {
			Log.debug(this, "Channel has been opened channelid="
					+ channel.getChannelId());
		}

		/**
		 * Do our sessions require x forwarding? If so then request and make
		 * sure our XForwarding Channel Factory is active.
		 */
		if (connection.getContext().getX11Display() != null) {

			String display = connection.getContext().getX11Display();

			int idx = display.indexOf(':');
			int screen = 0;
			if (idx != -1) {
				display = display.substring(idx + 1);
			}

			idx = display.indexOf('.');

			if (idx > -1) {
				screen = Integer.parseInt(display.substring(idx + 1));
			}

			byte[] x11FakeCookie = connection.getContext()
					.getX11AuthenticationCookie();
			StringBuffer cookieBuf = new StringBuffer();
			for (int i = 0; i < 16; i++) {
				String b = Integer.toHexString(x11FakeCookie[i] & 0xff);
				if (b.length() == 1) {
					b = "0" + b;
				}
				cookieBuf.append(b);
			}

			if (channel.requestX11Forwarding(false, "MIT-MAGIC-COOKIE-1",
					cookieBuf.toString(), screen)) {
				isXForwarding = true;
			}

		}
		return channel;
	}

	public SshClient openRemoteClient(String hostname, int port,
			String username, SshConnector con) throws SshException,
			ChannelOpenException {

		if (Log.isDebugEnabled()) {
			Log.debug(this,
					"Opening a remote SSH client from " + io.getHost() + " to "
							+ username + "@" + hostname + ":" + port);
		}
		SshTunnel tunnel = openForwardingChannel(hostname, port, "127.0.0.1",
				22, "127.0.0.1", 22, null, null);

		return con.connect(tunnel, username, buffered);

	}

	public SshClient openRemoteClient(String hostname, int port, String username)
			throws SshException, ChannelOpenException {
		return openRemoteClient(hostname, port, username, connector);
	}

	public SshTunnel openForwardingChannel(String hostname, int port,
			String listeningAddress, int listeningPort, String originatingHost,
			int originatingPort, SshTransport transport,
			ChannelEventListener listener) throws SshException,
			ChannelOpenException {

		ByteArrayWriter request = new ByteArrayWriter();

		try {

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Opening forwarding channel from "
						+ listeningAddress + ":" + listeningPort + " to "
						+ hostname + ":" + port);
			}

			Ssh2ForwardingChannel tunnel = new Ssh2ForwardingChannel(
					Ssh2ForwardingChannel.LOCAL_FORWARDING_CHANNEL, 32768,
					2097152, hostname, port, listeningAddress, listeningPort,
					originatingHost, originatingPort, transport);

			request.writeString(hostname);
			request.writeInt(port);
			request.writeString(originatingHost);
			request.writeInt(originatingPort);

			tunnel.addChannelEventListener(listener);

			openChannel(tunnel, request.toByteArray());
			return tunnel;
		} catch (IOException ex) {
			throw new SshException(ex, SshException.INTERNAL_ERROR);
		} finally {
			try {
				request.close();
			} catch (IOException e) {
			}
		}
	}

	public boolean requestRemoteForwarding(String bindAddress, int bindPort,
			String hostToConnect, int portToConnect,
			ForwardingRequestListener listener) throws SshException {

		ByteArrayWriter baw = new ByteArrayWriter();

		try {
			if (listener == null) {
				throw new SshException(
						"You must specify a listener to receive connection requests",
						SshException.BAD_API_USAGE);
			}

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Requesting remote forwarding from "
						+ bindAddress + ":" + bindPort + " to " + hostToConnect
						+ ":" + portToConnect);
			}

			baw.writeString(bindAddress);
			baw.writeInt(bindPort);
			GlobalRequest request = new GlobalRequest("tcpip-forward",
					baw.toByteArray());

			if (sendGlobalRequest(request, true)) {

				forwardingListeners.put(
						(bindAddress + ":" + String.valueOf(bindPort)),
						listener);
				forwardingDestinations.put(
						(bindAddress + ":" + String.valueOf(bindPort)),
						(hostToConnect + ":" + String.valueOf(portToConnect)));
				// Setup the forwarding listener
				return true;
			}
			return false;
		} catch (IOException ex) {
			throw new SshException(ex, SshException.INTERNAL_ERROR);
		} finally {
			try {
				baw.close();
			} catch (IOException e) {
			}
		}

	}

	public boolean cancelRemoteForwarding(String bindAddress, int bindPort)
			throws SshException {

		ByteArrayWriter baw = new ByteArrayWriter();

		try {

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Cancelling remote forwarding from "
						+ bindAddress + ":" + bindPort);
			}

			baw.writeString(bindAddress);
			baw.writeInt(bindPort);
			GlobalRequest request = new GlobalRequest("cancel-tcpip-forward",
					baw.toByteArray());

			if (sendGlobalRequest(request, true)) {

				forwardingListeners.remove(bindAddress + ":"
						+ String.valueOf(bindPort));
				forwardingDestinations.remove(bindAddress + ":"
						+ String.valueOf(bindPort));

				return true;
			}

			return false;
		} catch (IOException ex) {
			throw new SshException(ex, SshException.INTERNAL_ERROR);
		} finally {
			try {
				baw.close();
			} catch (IOException e) {
			}
		}
	}

	/**
	 * Additional method to open a custom SSH2 channel.
	 * 
	 * @param channel
	 *            the channel to open
	 * @param requestdata
	 *            the request data
	 * @throws SshException
	 * @throws ChannelOpenException
	 */
	public void openChannel(Ssh2Channel channel, byte[] requestdata)
			throws SshException, ChannelOpenException {
		verifyConnection(true);
		connection.openChannel(channel, requestdata);
	}

	/**
	 * Additional method to open a custom SSH2 channel.
	 * 
	 * @param channel
	 *            the channel to open
	 * @throws SshException
	 * @throws ChannelOpenException
	 */
	public void openChannel(SshAbstractChannel channel) throws SshException,
			ChannelOpenException {
		verifyConnection(true);
		if (channel instanceof Ssh2Channel) {
			connection.openChannel((Ssh2Channel) channel, null);
		} else {
			throw new SshException("The channel is not an SSH2 channel!",
					SshException.BAD_API_USAGE);
		}
	}

	/**
	 * Installs a custom channel factory so that the client may respond to
	 * channel open requests.
	 * 
	 * @param factory
	 *            the channel factory
	 * @throws SshException
	 */
	public void addChannelFactory(ChannelFactory factory) throws SshException {
		connection.addChannelFactory(factory);
	}

	public SshContext getContext() {
		return transport.transportContext;
	}

	/**
	 * Installs a global request handler so that the client may respond to
	 * global requests.
	 * 
	 * @param handler
	 *            the global request handler
	 * @throws SshException
	 */
	public void addRequestHandler(GlobalRequestHandler handler)
			throws SshException {

		if (Log.isDebugEnabled()) {
			String requests = "";
			for (int i = 0; i < handler.supportedRequests().length; i++)
				requests += handler.supportedRequests()[i] + " ";
			Log.debug(this, "Installing global request handler for "
					+ requests.trim());
		}

		connection.addRequestHandler(handler);
	}

	/**
	 * Sends a global request to the remote side.
	 * 
	 * @param request
	 *            the global request
	 * @param wantreply
	 *            specifies whether the remote side should send a
	 *            success/failure message
	 * @return <code>true</code> if the request succeeded and wantreply=true,
	 *         otherwise <code>false</code>
	 * @throws SshException
	 */
	public boolean sendGlobalRequest(GlobalRequest request, boolean wantreply)
			throws SshException {
		verifyConnection(true);
		return connection.sendGlobalRequest(request, wantreply);
	}

	public String getRemoteIdentification() {
		return remoteIdentification;
	}

	void verifyConnection(boolean requireAuthentication) throws SshException {
		if (authentication == null || transport == null || connection == null) {
			throw new SshException("Not connected!", SshException.BAD_API_USAGE);
		}
		if (!transport.isConnected()) {
			throw new SshException("The connection has been terminated!",
					SshException.REMOTE_HOST_DISCONNECTED);
		}
		if (!authentication.isAuthenticated() && requireAuthentication) {
			throw new SshException("The connection is not authenticated!",
					SshException.BAD_API_USAGE);
		}

	}

	public String getUsername() {
		return username;
	}

	public SshClient duplicate() throws SshException {

		if ((username == null || auth == null)) {
			throw new SshException(
					"Cannot duplicate! The existing connection does not have a set of credentials",
					SshException.BAD_API_USAGE);
		}

		try {

			if (Log.isDebugEnabled()) {
				Log.debug(this, "Duplicating SSH client");
			}

			SshClient duplicate = connector.connect(io.duplicate(), username,
					buffered, transport.transportContext);

			if (duplicate.authenticate(auth) != SshAuthentication.COMPLETE) {
				duplicate.disconnect();
				throw new SshException(
						"Duplication attempt failed to authenicate user!",
						SshException.INTERNAL_ERROR);
			}

			return duplicate;
		} catch (IOException ex) {
			throw new SshException(ex, SshException.CONNECT_FAILED);
		}
	}

	class ForwardingRequestChannelFactory implements ChannelFactory {

		String[] types = new String[] {
				Ssh2ForwardingChannel.REMOTE_FORWARDING_CHANNEL, "x11" };

		public String[] supportedChannelTypes() {
			return types;
		}

		/**
		 * <p>
		 * Create an instance of an SSH channel. The new instance should be
		 * returned, if for any reason the channel cannot be created either
		 * because the channel is not supported or there are not enough
		 * resources an exception is thrown.
		 * </p>
		 * 
		 * @param channeltype
		 * @param requestdata
		 * @return an open channel
		 * @throws ChannelOpenException
		 */
		public Ssh2Channel createChannel(String channeltype, byte[] requestdata)
				throws SshException, ChannelOpenException {

			if (channeltype
					.equals(Ssh2ForwardingChannel.REMOTE_FORWARDING_CHANNEL)) {
				ByteArrayReader bar = new ByteArrayReader(requestdata);
				try {

					String address = bar.readString();
					int port = (int) bar.readInt();
					String originatorIP = bar.readString();
					int originatorPort = (int) bar.readInt();

					String key = address + ":" + String.valueOf(port);
					if (forwardingListeners.containsKey(key)) {
						ForwardingRequestListener listener = (ForwardingRequestListener) forwardingListeners
								.get(key);
						String destination = (String) forwardingDestinations
								.get(key);
						String hostToConnect = destination.substring(0,
								destination.indexOf(':'));
						int portToConnect = Integer.parseInt(destination
								.substring(destination.indexOf(':') + 1));

						if (Log.isDebugEnabled()) {
							Log.debug(this,
									"Creating remote forwarding channel from "
											+ address + ":" + port + " to "
											+ hostToConnect + ":"
											+ portToConnect);
						}

						// create connection from here to end point of tunnel,
						// then pass to new Ssh2ForwardingChannel
						Ssh2ForwardingChannel channel = new Ssh2ForwardingChannel(
								Ssh2ForwardingChannel.REMOTE_FORWARDING_CHANNEL,
								32768, 2097152, hostToConnect, portToConnect,
								address, port, originatorIP, originatorPort,
								listener.createConnection(hostToConnect,
										portToConnect));

						listener.initializeTunnel(channel);

						return channel;

					}
					throw new ChannelOpenException(
							"Forwarding had not previously been requested",
							ChannelOpenException.ADMINISTRATIVIVELY_PROHIBITED);
				} catch (IOException ex) {
					throw new ChannelOpenException(ex.getMessage(),
							ChannelOpenException.RESOURCE_SHORTAGE);
				} catch (SshException ex) {
					throw new ChannelOpenException(ex.getMessage(),
							ChannelOpenException.CONNECT_FAILED);
				} finally {
					try {
						bar.close();
					} catch (IOException e) {
					}
				}

			} else if (channeltype.equals("x11")) {

				if (!isXForwarding)
					throw new ChannelOpenException(
							"X Forwarding had not previously been requested",
							ChannelOpenException.ADMINISTRATIVIVELY_PROHIBITED);
				ByteArrayReader bar = new ByteArrayReader(requestdata);

				try {

					String originatorIP = bar.readString();
					int originatorPort = (int) bar.readInt();

					String display = connection.getContext().getX11Display();

					int i = display.indexOf(":");
					String targetAddr;
					int targetPort;
					int num = 0;
					int screen = 0;
					if (i != -1) {
						targetAddr = display.substring(0, i);
						display = display.substring(i + 1);
						i = display.indexOf('.');
						if (i > -1) {
							num = Integer.parseInt(display.substring(0, i));
							screen = Integer.parseInt(display.substring(i + 1));
						} else
							num = Integer.parseInt(display);

						targetPort = num;
					} else {
						targetAddr = display;
						targetPort = 6000;
					}

					if (targetPort <= 10) {
						targetPort += 6000;
					}

					if (Log.isDebugEnabled()) {
						Log.debug(this,
								"Creating X11 forwarding channel for display "
										+ targetAddr + ":" + screen);
					}

					ForwardingRequestListener listener = connection
							.getContext().getX11RequestListener();

					Ssh2ForwardingChannel channel = new Ssh2ForwardingChannel(
							Ssh2ForwardingChannel.X11_FORWARDING_CHANNEL,
							32768, 32768, targetAddr, targetPort, targetAddr, // This
																				// will
																				// get
																				// set
																				// as
																				// the
																				// forwarding
																				// key
							screen, // This will get set as the forwarding key
							originatorIP, originatorPort,
							listener.createConnection(targetAddr, targetPort));

					listener.initializeTunnel(channel);

					return channel;

				} catch (Throwable ex) {
					throw new ChannelOpenException(ex.getMessage(),
							ChannelOpenException.CONNECT_FAILED);
				} finally {
					try {
						bar.close();
					} catch (IOException e) {
					}
				}
			}

			throw new ChannelOpenException(channeltype + " is not supported",
					ChannelOpenException.UNKNOWN_CHANNEL_TYPE);
		}
	}

	public int getChannelCount() {
		return connection.getChannelCount();
	}

	public int getVersion() {
		return 2;
	}

	public boolean isBuffered() {
		return buffered;
	}

	/**
	 * Returns the key exchange algorithm last used.
	 * 
	 * @return String
	 */
	public String getKeyExchangeInUse() {
		return (transport.keyExchange == null ? "none" : transport.keyExchange
				.getAlgorithm());
	}

	public SshKeyExchangeClient getKeyExchangeInstanceInUse() {
		return transport.keyExchange;
	}

	/**
	 * Returns the host key algorithm used in the last key exchange.
	 * 
	 * @return String
	 */
	public String getHostKeyInUse() {
		return (transport.hostkey == null ? "none" : transport.hostkey
				.getAlgorithm());
	}

	/**
	 * Get the cipher algorithm used to encrypt data sent to the server.
	 * 
	 * @return String
	 */
	public String getCipherInUseCS() {
		return (transport.encryption == null ? "none" : transport.encryption
				.getAlgorithm());
	}

	/**
	 * Get the cipher algorithm used to decrypt data received from the server.
	 * 
	 * @return String
	 */
	public String getCipherInUseSC() {
		return (transport.decryption == null ? "none" : transport.decryption
				.getAlgorithm());
	}

	/**
	 * Get the MAC algorithm used to verify data sent by the client.
	 * 
	 * @return String
	 */
	public String getMacInUseCS() {
		return (transport.outgoingMac == null ? "none" : transport.outgoingMac
				.getAlgorithm());
	}

	/**
	 * Get the MAC algorithm used to verify data sent by the server.
	 * 
	 * @return String
	 */
	public String getMacInUseSC() {
		return (transport.incomingMac == null ? "none" : transport.incomingMac
				.getAlgorithm());
	}

	/**
	 * Get the compression algorithm used to compress the clients outgoing data.
	 * 
	 * @return String
	 */
	public String getCompressionInUseCS() {
		return (transport.outgoingCompression == null ? "none"
				: transport.outgoingCompression.getAlgorithm());
	}

	/**
	 * Get the compression algorithm used to decompress the servers data.
	 * 
	 * @return String
	 */
	public String getCompressionInUseSC() {
		return (transport.incomingCompression == null ? "none"
				: transport.incomingCompression.getAlgorithm());
	}

	public String toString() {
		return "SSH2 "
				+ io.getHost()
				+ ":"
				+ io.getPort()
				+ " [kex="
				+ (transport.keyExchange == null ? "none"
						: transport.keyExchange.getAlgorithm())
				+ " hostkey="
				+ (transport.hostkey == null ? "none" : transport.hostkey
						.getAlgorithm())
				+ " client->server="
				+ (transport.encryption == null ? "none" : transport.encryption
						.getAlgorithm())
				+ ","
				+ (transport.outgoingMac == null ? "none"
						: transport.outgoingMac.getAlgorithm())
				+ ","
				+ (transport.outgoingCompression == null ? "none"
						: transport.outgoingCompression.getAlgorithm())
				+ " server->client="
				+ (transport.decryption == null ? "none" : transport.decryption
						.getAlgorithm())
				+ ","
				+ (transport.incomingMac == null ? "none"
						: transport.incomingMac.getAlgorithm())
				+ ","
				+ (transport.incomingCompression == null ? "none"
						: transport.incomingCompression.getAlgorithm()) + "]";
	}
}