Adding Telnet Support to SSHMenu

I received an email from an SSHMenu user who wanted to modify the program to add telnet support for connecting to network routers and switches. It seemed like a good case study to showcase how easy it is to change the behaviour of SSHMenu without modifying the code.

The goal is relatively simple — to have some connections use telnet instead of SSH. To achieve that goal we need to write some code which decides on a per-host basis which protocol to use, and plug that code in to SSHMenu.

Two solutions are presented here. The first is a quick and dirty solution that avoids modifying the user interface and gives a concise example of the steps required. The second solution is more complete and includes a modification to the user interface as well as the config file.

A Quick and Dirty Solution

To get something working quickly, we'll modify the menu behaviour so that the telnet protocol is used for connecting to any host which has a menu title starting with 'Telnet:'.

menu screenshot

The SSHMenu application class defines a method called ssh_command which is used to decide which command to use when connecting to a host. Normally, this method simply returns 'ssh' but we'll define an alternative method that returns 'telnet' for any host with a title that starts 'Telnet:'.

The first step is to create a Ruby source file that will contain our new code. This is the code which I chose to save in a 'ruby' directory under my home directory (full pathname: /home/grant/ruby/sshtelnetmenu1.rb):

require 'gnome-sshmenu'

module SSHTelnetMenu

  class App <GnomeSSHMenu::App

    def ssh_command(host)
      if host.title =~ /^Telnet:/
        return 'telnet'
      end
      return super
    end

  end

end

The new code defines a method in a class in a module. The module simply provides a namespace to ensure that our code does not conflict with code in other namespaces. The choice of module name is arbitrary and I used SSHTelnetMenu.

The class name is App, so the fully specified class name is SSHTelnetMenu::App The class inherits from the standard SSHMenu class GnomeSSHMenu::App. Although you could choose any class name you like, it probably makes sense to stick to the same naming scheme as the standard classes you're overriding.

The ssh_command method will be passed a host object (specifically, an object of class GnomeSSHMenu::HostItem). For our quick and dirty solution, the new method uses a regular expression to check the title of the host object and either returns 'telnet' or drops through to the ssh_command method inherited from the super class we're inheriting from.

That's really all the code that's required, now all that remains is to configure SSHMenu to use our new class. The SSHMenu config file is a YAML file called .sshmenu in your home directory. By default it will have an empty 'classes' definition that looks like this:

classes: {}

Which I changed to look like this (note the empty curly braces and the blank line were both removed and two new lines added):

classes:
  require: /home/grant/ruby/sshtelnetmenu1.rb
  app: SSHTelnetMenu::App

The 'require' line tells SSHMenu to load the source file containing the new class at startup. The 'app' line tells SSHMenu to use our new SSHTelnetMenu::App class when creating the main application object.

To test the modifications, we can run the command: sshmenu-gnome which will pop-up a new standalone application window containing the familiar 'SSH' button. Once we're satisfied that it's all working, we can update the version running in the GNOME panel by removing the applet and re-adding it.

A More Complete Solution

While the solution described above does work, it's a bit of a 'dirty hack'. A more complete solution involves adding a 'Use Telnet' checkbox to the host definition dialog and using the checkbox state to control the selection of SSH vs Telnet:

host dialog screenshot

The good news is that the way to achieve this follows the pattern in the first solution — we simply need to override a few more methods in a few more classes.

The Ruby code looks like this:

require 'gnome-sshmenu'

module SSHTelnetMenu

  class App <GnomeSSHMenu::App

    def ssh_command(host)
      if host.use_telnet
        return 'telnet'
      end
      return super
    end

  end

  class HostItem <GnomeSSHMenu::HostItem

    attr_accessor :use_telnet

    def initialize(h={})
      super
      @use_telnet = true if h['use_telnet']
    end

    def to_h
      h = super
      h['use_telnet'] = use_telnet
      return h
    end

  end

  class HostDialog <GnomeSSHMenu::HostDialog

    def add_other_inputs
      super
      @use_telnet = Gtk::CheckButton.new( "Use telnet rather than SSH", false)
      @use_telnet.active = true if @host.use_telnet
      @body.pack_start(@use_telnet, false, true, 0)
    end

    def dialog_to_host(host=nil)
      host = super(host)
      host.use_telnet = @use_telnet.active?
      return host
    end

  end

end

The new configuration looks like this:

classes:
  require: /home/grant/ruby/sshtelnetmenu2.rb
  app: SSHTelnetMenu::App
  app.dialog.host: SSHTelnetMenu::HostDialog
  app.model.hostitem: SSHTelnetMenu::HostItem

This time we're overriding methods in three different classes:

The HostItem class is a container for the attributes of a host definition. Our code adds a use_telnet accessor method to represent the state of the checkbox. The constructor is modified to initialise this property when the config is read in and the to_h method is modified to include the property when the object is serialised to a hash for saving in the config file.

The HostDialog class manages the dialog box through which the user edits a host definition. The add_other_inputs method is modified to add an extra checkbox to the dialog and initialise its state from the host item's use_telnet property. The dialog_to_host method is modified to transfer the checkbox value back into the host object when the 'OK' button is clicked.

Finally, in the App class, the ssh_command method is modified to make a Telnet vs SSH decision based on the value of the host item's use_telnet property.

Summary

Using the techniques described above, you can modify the behaviour of your SSHMenu without modifying the base code. This means that you should be able to install future versions of SSHMenu and still retain your local modifications.

The SSHMenu Hacker's Guide has more information on classes you can override. Refer to the SSHMenu source documentation to learn about methods you can override.