We need to understand the SSH channel process in order to continue, so what are channels? All terminal sessions, forwarded connections, etc. are channels. Either side may open a channel and multiple channels are multiplexed into a single connection. Channels are flow-controlled, that is no data may be sent to a channel until a message is received to indicate that window space is available.
Channels can be whatever you want them to be, there are many channel types already defined within the SSH protocol specification, such as the 'session' or 'tcpip-forward' channels. These are the mechanism that provide the transport for your session and port forwarding data.
When you want to open a channel you send a request to the SSH server, the server will then either respond to that request with a confirmation or failure message. Your channel is identified by a name, for instance lets say we want to create a channel to echo information back to the client, we would call the channel "echo@3sp.com";. You should always name your channel using the name@domain syntax.
J2SSH provides a mechanism for developing custom channels, the com.sshtools.j2ssh.connection package contains a number of classes to help you. First the Channel class is the abstract base for all channels. Lets take a look at its abstract methods, the first method is self explainitory, it returns the name/type of the channel.
public String getChannelType() { return "echo@3sp.com"; }
The next two methods provide the settings for the flow control. The values returned by the following mehtods set the boundary for the window space. The channel will never provide more window space than the maximum and will increase window space automatically (back up to the maximum) when the minimum is reached.
protected int getMinimumWindowSpace() { return 1024; } protected int getMaximumWindowSpace() { return 65535; }
The maximum packet size setting is the maximum amount of data that the remote side can send in one single packet. This value should not exceed the maximum window space.
protected int getMaximumPacketSize() { return 32768; }
When a request is made to open a channel, the client can send some data with the open request so that the server may process the request based on additional information. Your channel should return this information (if any) in the getChannelOpenData method, if there is no data simply return null.
public byte[] getChannelOpenData() { return null; }
When the server confirms that the channel is open it can also provide data in response to the channel open information. It does so by returning this information in the getChannelConfirmationData method. Again if no data is required to be sent to the remote side simply return null.
public byte[] getChannelConfirmationData() { return null; }
Once the channel has been opened, the channel mechanism calls the channels onChannelOpen method. You should not perform any expensive processing in this method since it will lock up the protocol and you will not be able to send data.
protected void onChannelOpen() throws java.io.IOException { /** Your channel is open so do stuff if you want **/ }
Once the channel is open you can send data using the channels sendChannelData method, this type of data is received by the channel with the following method.
protected void onChannelData(SshMsgChannelData msg) throws java.io.IOException { // Channel data has arrived, process it! }
There is also an extended data channel which can be used, the extended data has a type field which can be used to identify different application defined data types. This is received with the onChannelExtData method and sent with sendChannelExtData.
protected void onChannelExtData(SshMsgChannelExtendedData msg) throws java.io.IOException {
/**@todo Implement this com.sshtools.j2ssh.connection.Channel abstract method*/
}
There is also a request mechanism seperate to the channel data which can be used. This provides named based requests. For example in our echo channel we could have a request to turn echo on and off. Requests are recieved with the onChannelRequet method and sent with sendChannelRequest.
protected void onChannelRequest(String requestname, boolean wantreply, byte[] requestdata) throws java.io.IOException { /**@todo Implement this com.sshtools.j2ssh.connection.Channel abstract method*/ }
When you no longer wish to send data you can set the local side to EOF by using setLocalEOF. When the remote server sends EOF the onChannelEOF method is called.
protected void onChannelEOF() throws java.io.IOException { /**@todo Implement this com.sshtools.j2ssh.connection.Channel abstract method*/ }
Finally, to close the channel you can use the close method. When the channel is closed by either side the onChannelClose method is called.
protected void onChannelClose() throws java.io.IOException { /**@todo Implement this com.sshtools.j2ssh.connection.Channel abstract method*/ }
Ok so now we know the basic structure how do we implement our channel and use it from either side of the connection? Heres our server side channel implelentation. This simply returns any data sent to it if echo is on.
public class EchoChannel extends Channel { boolean echo = true; public EchoChannel() { } public String getChannelType() { return "echo@3sp.com";; } protected void onChannelRequest(String requestname, boolean wantreply, byte[] requestdata) throws java.io.IOException { if(requestname.equals("echo-off@3sp.com";)) { echo = false; if(wantreply) connection.sendChannelRequestSuccess(this); return; } else if(requestname.equals("echo-on@3sp.com";)) { echo = true; if(wantreply) connection.sendChannelRequestSuccess(this); return; } if(wantreply) connection.sendChannelRequestFailure(this); } protected void onChannelExtData(SshMsgChannelExtendedData msg) throws java.io.IOException { } protected void onChannelData(SshMsgChannelData msg) throws java.io.IOException { if(echo) sendChannelData(msg.getChannelData()); } protected int getMaximumPacketSize() { return 32768; } protected void onChannelEOF() throws java.io.IOException { } protected void onChannelClose() throws java.io.IOException { } public byte[] getChannelOpenData() { return null; } protected int getMinimumWindowSpace() { return 1024; } protected int getMaximumWindowSpace() { return 65535; } protected void onChannelOpen() throws java.io.IOException { } public byte[] getChannelConfirmationData() { return null; } }
Now we need to configure the server to accept the channel and create an instance of our EchoChannel when a request is made. To do this we will need to create a ChannelFactory that can create the channel, this is a simple interface
public interface ChannelFactory { public Channel createChannel(String channelType, byte[] requestData) throws InvalidChannelException; }
So lets create an EchoChannelFactory implementation
public class EchoChannelFactory implements ChannelFactory { public Channel createChannel(String channelType, byte[] requestData) throws InvalidChannelException { if(channelType.equals("echo@3sp.com";)) { return new EchoChannel(); } throw new InvalidChannelException("Only echo channels allowed by this factory"); } }
So now were ready to configure the server. In my previous articles you will remember we implemented the configureServices method of the SshServer? To allow this channel to be opened, we simply add the following line to the configureServices implementation
connection.addChannelFactory("echo@3sp.com";, new EchoChannelFactory());
Your server should now be configured to support your channel. So lets look at how to invoke the channel from the client.
First we need to create a client side implementation of the channel, but we need it to be simpler with a set of IOStreams perhaps??? Well the hard work is already done for you, take a look at the IOChannel class. This provides an InputStream and OutputStream for the channels data. Heres the implementation, it has less methods to implement since the channel data is now handled by the parent class.
public class EchoChannelClient extends IOChannel { public EchoChannelClient() { } public String getChannelType() { return "echo@3sp.com";; } protected void onChannelRequest(String requestname, boolean wantreply, byte[] data) throws java.io.IOException { if(wantreply) connection.sendChannelRequestFailure(this); } protected int getMaximumPacketSize() { return 32768; } public byte[] getChannelOpenData() { return null; } protected int getMinimumWindowSpace() { return 1024; } protected void onChannelOpen() throws java.io.IOException { } protected int getMaximumWindowSpace() { return 65535; } public byte[] getChannelConfirmationData() { return null; } }
We need to add a method for turning the echo on and off.
public void setEcho(boolean echo) throws java.io.IOException { if(echo) connection.sendChannelRequest(this, "echo-on@3sp.com";, false, null); else connection.sendChannelRequest(this, "echo-off@3sp.com";, false, null); }
Now were ready to go since any data we write to the channels outputstream, which we obtain by using getOutputStream will be returned to the InputStream if echo is on.
To use the channel using an SshClient instance simply
EchoChannelClient echo = new EchoChannelClient(); if(ssh.openChannel(echo)) { // Channel is open echo.getOutputStream().write("hello world!".getBytes()); // Read it back from the inputstream byte[] buf = new byte[32]; echo.getInputStream().read(buf); }
So of course your requirements are probably much more complex, if you want the server to handle IOStreams you can use the IOChannel instead of the Channel, in the end its up to you....... with a little imagination ;-)