In the previous part of this article I showed you how to secure your client-application server by using J2SSH to create an embeded tunnel to an SSH server installed on the same host as your server. Whilst the tunnel is secure it does not provide access to the strong public key authentication that SSH provides. In this article we take no prisoners, were going to implement an SSH server and transfer your existing application server functionality into the SSH server itself so that your application can take advantage of public key authentication and other SSH services such as SFTP.
Starting the basic framework server
First things first, even before you think about how to get your
existing application functionality into the SSH server, you need to get the
server itself running and accepting each connection and authenticating the user.
Make sure you have the source version of J2SSH since we're going copy the com.sshtools.daemon.SshDaemon
source code to create your server.
If you examine this class you will see that it has a main method and so we're going to use this class to configure, start and stop your server.Lets not worry about what the SshDaemon is doing right now, lets just make sure we can start and stop the server. You may have noticed that the main method allows a couple of command line arguments -start and -stop. No guessing for what these do! Before we start the server there are a couple of configuration items we need to attend to. First of all the server needs a public key to identify itself to clients. Generate a key pair using the J2SSH command line utility ssh-keygen.
¢ ssh-keygen.bat -t dsa -b 1024 server_host_key
When prompted to enter a passprase simply press return and confirm that you do not want to encrypt the private key, server host keys cannot be encrypted since there is no where to store the passphrase!
Copy the host key into the J2SSH conf directory, whilst you're
in there open up the server configuration file server.xml and I'll take you
over some of the other options. Make sure that there is a
<ServerHostKey PrivateKeyFile="server_host_key"/>
Next make sure that the
element is
set to the port you wish your server to bind, if you have another SSH server
running you might want to change this to a non standard port for now.
Finally the <CommandPort>
element is the
local port which the server listens for commands, there is only one command
currently implemented and that is stop.
You should now be able to start the server by executing the SshDaemon class, you will need to set the sshtools.home System property to point to the root installation directory of J2SSH. This will be used to find the XMl configuration files. Once you have the server running (remember to pass in the -start option on the command line) it should accept connections, but if you connect and try to authenticate it will fail because there is no authentication provider installed. So that's our next step.
Creating an authentication providerThe authentication provider allows you to define how users are authenticated when they connect to the SSH server. Your application may already have a mechanism for this, perhaps by searching a database table, or an encrypted password file. In any case you will want to transfer this process to the authentication provider, since I cannot help you on that front, I will show you how to implement a basic mechanism and explain the methods you are required to provide. To get things moving perhaps you should just implement this with me to see how the process fits together.
The abstract NativeAuthenticationProvider
is where
we start, we are going to extend this class to implement an authentication provider
that examines the user name and logs them into the system. For simplicity we
will hard code this in this example.
class MyAuthenticationProvider extends NativeAuthenticationProvider { }
Now lets look at the methods we need to implement, first there
is getHomeDirectory
. This should return the home directory of the
user whose username is passed as a parameter into the method. This will be used
for public key configuration and also servers as the default directory for the
SFTP server that the framework will provide.
public String getHomeDirectory(String username) throws IOException { return "/home/users/martianx"; }
There are two logonUser
methods that we need to
implement next, the first provides a username and password which should be checked.
If the account exists and the password is correct you should perform any processing
that would log the user into the system and return true to indicate that the
logon was a success. Optionally you can throw a password change exception if
the users password has expired (more about that later).
public boolean logonUser(String username, String password) throws PasswordChangeException, IOException { return username.equals("martianx") && password.equals("foo")); }
The second logonUser
method only provides the username.
This is called when a user log's into the system using public key authentication.
When the method is called the server has already verified the identity of the
user through their public key so you should automatically log the user in without
restriction.
public boolean logonUser(String username) throws IOException { return true; // This is for public key authentication and will only // be called once a key has been verified }
The logoff
User method simply should log the user
out of the system, performing any necersary processing that your application
server requires. In our example we will do nothing
public void logoffUser() throws IOException { // do nothing yet }
Finally the changePassword
method is called if
you had previously had thrown a PasswordChangeException
from the
logonUser
method. This method should change the password and return
its success (or failure). Our example will return false since we do not support
password change yet.
public boolean changePassword(String username, String oldpassword, String newpassword) { return false; // We dont support this yet }
So that's the authentication provider implemented, you should have a good idea about how to go about integrating your servers authentication mechanism now? But first we need to have a look at this provider in action, so the next step is to configure the server to load the provider at startup.
The platform configuration file
The providers are configured in a platform configuration file.
The actual name of the file should be set to your application's name, but an
example is provided in the J2SSH distribution and its called platform.xml
.
Lets copy the file and create myplatform.xml
and configure it for
our new authentication provider.
In the file locate the
element and enter the FQN of the authentication implementation we just created.
<NativeAuthenticationProvider>com.myplatform.MyAuthenticationProvider</NativeAuthenticationProvider>
Save the file and before we start the server we need to make sure that the server will load the provider by setting a VM parameter, use
-Dsshtools.platform=myplatform.xml
Now start the server and try to connect using a standard SSH client and the authentication information we set in our example. The connection will still fail but you should get prompted for autehntication information and see an error message, for example using Putty as a client will probably show a pty refused message. This is actually a success since the pty is only requested once authentication has succeeded.
The next step is to remove this error message and were going
to acheive that by creating a NativeProcessProvider
.
Creating a NavtiveProcessProvider
The NativeProcessProvider allows the server to provide shell access and other session based services such as command execution. But you don't want a terminal I hear you say? Well that's fine but since your using the SSH framework your going to have to provide one, and you never know what you might be able to do in the future with a little imagination. For example one of the things I like to do is to provide terminal access to user accounts for self serve account maintenance, allowing them to login using a standard SSH client and change their password account details. It's always a good idea to allow some form of access into your application server that any standard client can access. What if your out of the office and really need to log in to change something, but don't have your laptop, connect using the SSHTerm applet and your connected again!
So what we are going to implement today is a basic terminal
that outputs the message "This server does not provide shell access"
and then disconnects. Let's start by declaring our class which extends NativeProcessProvider
public class UnsupportedShellProcess extends NativeProcessProvider { private static String message = "This server does not provide shell access"; }
An instance of this class will be created for each session that is created, the process is required to provide the standard set of IO streams stdin, stdout and stderr. We will simply use a dynamic buffer class that J2SSH (which is similar to Piped streams) that will allow us to output something from the other end of the stream which will be read by the connected client.
import com.sshtools.j2ssh.io.DynamicBuffer; DynamicBuffer stdin = new DynamicBuffer(); DynamicBuffer stderr = new DynamicBuffer(); DynamicBuffer stdout = new DynamicBuffer(); public InputStream getInputStream() throws IOException { return stdin.getInputStream(); } public OutputStream getOutputStream() throws IOException { return stdout.getOutputStream(); } public InputStream getStderrInputStream() { return stderr.getInputStream(); }
When a command or shell is executed, the framework first requests
that your provider create the process by calling the createProcess
method, this method should initialize your provider with command and environment
provided BUT NOT start the command. The framework will then start it sepeartley
with a call to start
. Since were writing a basic implementation
we dont need to setup the process so were ust going to return true to indicate
that the command is reading to start.
public boolean createProcess(String command, Map environment) throws IOException { // Of course this could be used to provide a wealth of features // according to the executed command and the environment return true; }
Now when the process is started we output our message
public void start() throws IOException { stdin.getOutputStream().write(message.getBytes()); }
Next the process
is required to provide a default terminal provider, this is used in place of
a command when the user requests to start a shell and so will be the command
argument when createProcess
is called, were not going to read this
so just supply any string.
public String getDefaultTerminalProvider() { return "UnsupportShell"; }
The client may request a pseudo terminal, again this is for a more advanced implementation than we need so in order to make sure that no client bombs out because we don't allocate one, we implement these methods to show we support and have allocated a terminal.
public boolean supportsPseudoTerminal(String term) { return true; }
public boolean allocatePseudoTerminal(String term, int cols, int rows, int width, int height, String modes) { return true; }
The kill method should end the process, we will simply close all the buffers
public void kill() { try { stdin.close(); } catch (IOException ex) { } try { stdout.close(); } catch (Exception ex1) { } try { stderr.close(); } catch (Exception ex2) { } }
We need to provide the server with an evaluation on the process to determine whether its still active. We will return active whilst the whole message has not been read
public boolean stillActive() { try { return stdin.getInputStream().available() > 0; } catch (IOException ex) { return false; } }
Finally this method should wait for the process to complete and then return the exit code. We are already planning on deprecating this method to provide a callback method but since its still here we need to implemenet it even if it is rather a crude implementation. We will simple wait for the message to be read and then return.
public int waitForExitCode() { try { while (stdin.getInputStream().available() > 0) { try { Thread.sleep(1000); // Crude but necersary } catch (InterruptedException ex) { } } } catch (IOException ex1) { } return 0; }
As you can see quite an easy implementation, when a connection is made that requests a shell or to execute a command the command will simply write out the message and once the message has been read the process will close.
To configure this in the platform configuration file you should
set the
"This server does not provide shell access"
Now you should have
a working SSH server which also provides SFTP access to your users. In the final
part of this article I will show you how to implement an SSH channel and configure
your client and server so that they directly talk to each other through SSH.