SSHTools Knowledgebase
Information and FAQs about SSHTools products
  
Search  
   
Browse by Category
SSHTools Knowledgebase .: J2SSH Maverick .: Tutorials .: Remote Forwarding VNC and RDP

Remote Forwarding VNC and RDP

Pre-Requisite
J2SSH Maverick API
Target Host running SSH Server
Local loopback connections must be enabled on SSH server
Either a Microsoft Windows OS with Remote Desktop Protocol (RDP) also known as Microsoft Windows Terminal Services also known as Remote Desktop Connection running AND OR,
VNC service running on target client and remote client (clients can be running Microsoft or Unix Operating System).

Resources
OpenSSH Free open source SSH server http://www.openssh.com/
TightVNC Open source VNC ne and a remote target host machine. Unlike local forwarding where the client acts as the listening process, here the roles are reversed and it is the remote target that acts as the listener of any communication request. The practical implication of this is that a remote user can connect to a central company networked SSH server and use it as a go between to access another client machine within that network. In this example we demonstrate this simple procedure by utilising virtual remote accessing software, VNC and RDP, to access a client machine across the network.

How It Works
Establishing A Session
The first action that is carried out is establishing a SSH session with the host.

con = SshConnector.getInstance();
ssh = connectionSetup(con);
new InternetAddress(to)); message.setSubject(emailSubject); message.setText(emailBody); // Send message Transport.send(message);

Details of JavaMail and JAF are beyond the scope of this article, for further details please refer the Resource section of this article.

Opening a Shell
Once we have established a successfully authenticated connection to our target host we are ready to get to work. The first thing we need is an isntance of the Shell class.

            //   Start a session and do basic IO
            if (ssh.isAuthenticated()) {
                shell = new Shell(ssh);

In order to open a correctly configured Shell object we need to provide the constructor our authenticated SshClient.

Displaying Status Information
The next thing we do is ascertain configuration information of our connected host using the Shell::getEnvrionment() method.

                if(shell.getEnvironment().hasEnvironmentVariable("HOSTNAME"))
                    System.out.println("We are connected to " + shell.getEnvironment().getEnvironmentVariable("HOSTNAME"));
                
                System.out.println("Remote operating system is " + shell.getEnvironment().getOperatingSystem());
                
                boolean isWindows = shell.getEnvironment().getOSType()==ShellEnvironment.OS_WINDOWS;
                
                if(shell.getEnvironment().hasEnvironmentVariable("USERPROFILE")) {
                    System.out.println("User home is " + shell.getEnvironment().getEnvironmentVariable("USERPROFILE"));
                } else if(shell.getEnvironment().hasEnvironmentVariable("HOME")) {
                    System.out.println("User home is " + shell.getEnvironment().getEnvironmentVariable("HOME"));
                }            

The Shell::getEnvironment() method returns a ShellEnvironment object populated with environment values. Firstly we ask for the HOSTNAME of our target machine,

                if(shell.getEnvironment().hasEnvironmentVariable("HOSTNAME"))
                    System.out.println("We are connected to " + shell.getEnvironment().getEnvironmentVariable("HOSTNAME"));

Not every Target Host has a HOSTNAME environment variable which is why we only display the field if it exists, if(shell.getEnvironment().hasEnvironmentVariable("HOSTNAME").
We then retrieve the Operating System of the Target Host.

 
                boolean isWindows = shell.getEnvironment().getOSType()==ShellEnvironment.OS_WINDOWS;

Finally we retrieve the User specific information.

                if(shell.getEnvironment().hasEnvironmentVariable("USERPROFILE")) {
                    System.out.println("User home is " + shell.getEnvironment().getEnvironmentVariable("USERPROFILE"));
                } else if(shell.getEnvironment().hasEnvironmentVariable("HOME")) {
                    System.out.println("User home is " + shell.getEnvironment().getEnvironmentVariable("HOME"));
                }     

This powerful method not only enables interrogation of the host environment but also the authenticated user. Based on the outcome of this information we are able to execute the appropriate Shell commands and scripts.

Unix Administration Tasks
We have established a secure connection to our target, opened a Shell object and have retrieved environment and user details. Now we are ready to execute our tests.

                //   Commands only executed for Unix OS
                if(!isWindows) {
                    //   Execute a script
                    traverseDirectory(shell, mailClient);
                    
                    //   Execute a script
                    cpuUsage(shell, mailClient);
                    
                    //   Eexcute a command
                    diskUsage(shell, mailClient);
                    
                    Vector hostnames = new Vector();
                    hostnames.add("192.168.1.10");
                    hostnames.add("192.168.1.108");
                    
                    //   Run command that returns different results
                    pingRemoteHosts(hostnames, shell, mailClient);
                    
                }

The tests have been written as though the user is an Administrator of the host machine and needs to run regular checks for simple things as user home space, excessive processes. The tests are a basic implementation of what a Unix Administrator might wish to execute.
All tests have been stored within their own methods for clarity and are all written for a Unix host machine. Using the boolean isWindows variable that was set earlier we are able to restrict the execution of these tests to a Unix host only.

Traverse Directory
This first method simply demonstrates the idea of running a script.

//   Execute a simple script than prints a directory tree 
        ShellProcess process = shell.execute("bash tree.sh");
        
        try{

            process.expect("Total directories", 1000, true);
            System.out.print(process.toString());
        } catch (ShellTimeoutException ex1) {

We simply execute the script ("bash tree.sh") and inform the running thread when to terminate execution, process.expect("Total directories", 1000, true).
The command sets a background thread running which begins to retrieve data from the active session. This data is fed into the ShellProcess object upon which we determine when the data retrieval process should end, the general principle is to continue running between a start and stop prompt. The first parameter, ("Total directories" tells the ShellProcess to end when the given pattern is retrieved. The second part of the OR condition is to terminate data retrieval instead after a given duration, 1000. If this second condition is met first the Shell terminates the current running process producing the second command prompt. This is used in the event the executing command hangs, one is able to force termination.
The final parameter true) informs the ShellProcess to consume unmatched lines. Whenever the ShellProcess::readLine() method is used to fetch session output, only pattern matched lines are returned, however the ShellProcess continues to maintain the entire session output, but unmatched lines are simply ignored by the method call.

After the process has returned the results are output,

            System.out.print(process.toString());

CPU Usage
This method also executes a script, ("./xcesCPU"), only this time we are extracting and processing the returned output further.

            //    Simple script to determine excessive CPU usage
            process = shell.execute("./xcesCPU");

The script executes the following ps command, ps -e -o pcpu -o pid -o user -o args | awk '$1 > 70 && $3 !="root" {print "USER "$3 " PROCESS NAME " $4 " PROCESS ID "$2}' filtering out processes that exceed 70% CPU usage. The result of which are sent to the awkinterpreter for further pre-processing.
The pre-processed output before the Java code begins to use it can be seen below,


In order to extract this data from the ShellProcess object we use the ShellProcess::readLine() method.

            //   Evaluate each line
            while((line = process.readLine())!= null) {

Becasue we havent used the ShellProcess::expect() with consumeUnmatchedLines set to true this method will retrieve every single processed line. Since our script automatically filters out unmatched lines we dont need to worry.

For each line we extract neccesary data.

                //   User
                offendingUser = line.substring(line.indexOf(USER_KEYWORD) + USER_KEYWORD.length(), line.indexOf(PROCESS_KEYWORD)).trim();

                //   Email Address
                offendingEmailAddress = offendingUser+COMPANY_EMAIL_ADDR;
                
                //   Process Name
                processName = line.substring(line.indexOf(PROCESS_KEYWORD) + PROCESS_KEYWORD.length(), line.indexOf(PID_KEYWORD)).trim();

                //   Process Id
                pid = line.substring(line.indexOf(PID_KEYWORD) + PID_KEYWORD.length());

Since the script pre-processes each line with additional tags; USER, NAME, PROCESS ID, we are able to use these as a means of extracting out required data, line.substring(line.indexOf(USER_KEYWORD) + USER_KEYWORD.length(), line.indexOf(PROCESS_KEYWORD).

The final step in this method is to email the owner of each identified process with a stern reminder that their process is hogging CPU time.

                //   Email user with extracted information
                mailClient.sendMail(offendingEmailAddress, "Excessive Process", "Process: "+processName+" Pid: "+pid);

We are able to extend the functionality by incorporating such additional code as resources as JavaMail.

Below you can see the actual output returned by this method, the MailClient outputs the content of the MimeMessage object it is ready to send to the mail server, this of course would then be sent to the users email inbox.

Disk Usage
In this test we are going to actually execute the command directly as oppose to the previous two tests which have relied on scripts.

            process = shell.execute("du --max-depth=1 home |sort -g -r | awk '$1>100000 {print $1 " DIR: "$2 " "+THRESHOLD_PATTERN_MATCH+""}'");

The command determines disk usage space and uses awk to pre-process the output before returning the results to the ShellProcess. The command finds any user HOME space that exceeds 100Mb. The pre-processed output before it is processed by our code can been seen below,

Once again we use the ShellProcess::expect() method to terminate our command.

                process.expect(THRESHOLD_PATTERN_MATCH, 10000, true);

The termianting pattern we are looking for is, THRESHOLD_PATTERN_MATCH="ABOVE THRESHOLD!!", the same pattern we used in the pre-processing of the result, print $1 " DIR: "$2 " "+THRESHOLD_PATTERN_MATCH+"". The processing time is set to, 1000, with consumeUnmatchedLines set to true.

We query the ShellProcess object for the number of lines returned. This will be used as the controller for our data processing block.

            int lineCount = process.getLineCount();
            for(int i=1; i< lineCount; i++) {            

The data processing block follows the same premise as all the others, the details of the user name are extracted and concatenated to a company standard email address.

                    offendingUser = line.substring(line.indexOf(HOME_KEYWORD)+HOME_KEYWORD.length(), line.indexOf(THRESHOLD_PATTERN_MATCH));
                    //   Append standard company email address 

                    offendingUser = offendingUser.trim()+COMPANY_EMAIL_ADDR;

Of course in a real world situation the email address of each offending user would be a little more detailed than their Unix account. Once again we extend the versatility a little by using JavaMail to email the offending users.

                    emailAddresses.add(offendingUser);

The output from this method is again produced by the MailClient object before the MimeMessage is sent to the SMTP mail server.


Ping Remote Hosts
In this test we are trying to communicate with a selection of remote hosts from our target machine. The example is a simple means of showing how, based on a given return value from ShellProcess class, we can branch off into other lines of processing, building up an entire hierarchy of events and actions based on varying returned values.

    private static void pingRemoteHosts(Vector hostnames, Shell shell, MailClient mailClient) throws Exception{ 

The list of remote hosts to communicate with as part of the method call. This example simply contains two ip addresses but in a real world scenario this Vectorwould be populated with all networked resources.

We execute the standard Unix ping command and use the ShellProcess::expect() method to terminate on the pattern, STATISTICS_PATTERN_MATCH = "ping statistics ".

                    process = shell.execute("ping -c3 "+hostname);
                    process.expect(STATISTICS_PATTERN_MATCH, 10000, true);

If the remote machine is not reachable ping sometimes hangs so we also specify a 10000 timeout value. We have also specified that consumeUnmatchedLines be consumed by the process. This should make our data processing block a little quicker as it is using the ShellProcess::readLine() method. As mentioned earlier consuming unmatched lines enables the readLine() to return only matched lines. So our next tasks is to begin processing our results.

                while ((line = process.readLine())!= null) {

                //   Extract the line which contains the statistics
                
                    if (line.startsWith(PACKETS_PATTERN_MATCH)) {
                        System.out.println("line is:"+line);
                        
                        if (line.contains("errors")) {

                            loss = Integer.valueOf(line.substring(line.indexOf(" ", line.indexOf("errors")), line.indexOf("%")).trim()).intValue();
                        } else {

                            loss = 0;
                        }

From the returned line we identify whether or not the remote target was reachable. If any communication problems do occur ping returns as part of its statistics summary an error value. We simply look for this keyword. If no errors occurred we simply set the loss value to 0.

From this loss value we are in a position to activate a number of branches based on this value. More complex and systems would use this and other returned values to perform various other checks and ping the remote host by other means and even try to restart the remote host.
To show all the varying idea and complexities that can be conjured up is not within the scope of this article, this is left up to the ideas of the individual. To give a simple example of this our code has two branches. Firstly if connection is established successfully interrogate the listening ports,

                        } else if (loss == 0) {

                            // Branch 2
                            //   Check ports
                            try{
                                process = shell.execute("nmap -sT "+hostname);
                                process.expect(NMAP_PATTERN_MATCH, 20000, false);

With a successful connection we simply email the results of nmap to the administrator in charge of this target machine.


Secondly if the remote target is not accessible we email the Administrator of the remote host informing them of the failed communication.
                        //   Branch 1
                        if (loss == 100) {

                            //   Inform administrator
                            mailClient.sendMail("admin"+COMPANY_EMAIL_ADDR, SUBJECT_FAIL, process.toString());

As per all the example we use the MailClient to output the email content before it is sent onto the SMTP mail server.


Summary
This code demonstrated how the Shell and ShellProcess classes can be used to interact with an active session to allow for a more interactive session. The methods have shown the various options that are available to a developer; the first demonstrating the execution of a standalone script, the second, a script which takes the returned output and processes it further. The third method highlighted the execution of actual commands as oppose to scripts and the fourth and final example method demonstrated that once we have control of the session's output we are able to expand the code into triggering different events and actions. With the aid of additional APIs, the article highlighted other means of expanding the versatility of these classes. In a real world situation with a little thought one can quite easily make the functionality into very powerful systems, not only can we interact with our session but have the Java API to further control and enhance processing of the output.

How To Run This Code
This attached zip file contains several source files. Unzip these into a new Java project and build. In addition the zip file contains two scripts, these should be place in the home directory of the user that will be authenticated onto the Target Host

2005 SSHTools Ltd, All Rights Reserved


How helpful was this article to you?

User Comments

Add Comment
No comments have been posted.


powered by Lore
© 2008 SSHTools Ltd. All Rights Reserved