Class SSHMenu::SetupWizard
In: lib/sshmenu.rb  (Git)
Parent: Object

The SSHMenu::SetupWizard class is used only once - when the menu program is first run and the config file does not yet exist. The role of the ‘wizard’ is to pull hostnames out of the user‘s .ssh/known_hosts file and add them to the menu. The user may chose to skip this process and manually add the required hosts.

Sadly this process turns out to be not very useful on systems with the HashKnownHosts option is enabled.

Methods

Public Instance methods

This is the main entry point. The SSHMenu::Config class will construct a SetupWizard object and then call this method on it. This method will call run_wizard to do the work and then return a list of SSHMenu::HostItem objects (possibly an empty list).

[Source]

# File lib/sshmenu.rb, line 2741
    def autoconfigure(config)
      @imported             = {}
      @hashed_hosts_skipped = 0
      @known_hosts_file     = config.home_dir + '.ssh/known_hosts'
      return unless @known_hosts_file.readable?
      return unless run_wizard
      host_list = []
      @imported.keys.sort.each do |name|
        host_list << {
          'type'      => 'host',
          'title'     => name,
          'sshparams' => "-AX #{@imported[name]}"
        }
      end
      return host_list
    end

Given a list of addresses, returns the FQDN of the first one to resolve or if none resolve, returns the last name from the list (on the assumption it was added most recently).

[Source]

# File lib/sshmenu.rb, line 3031
    def best_address(*aliases)

      # Return the first one that successfully resolves
      aliases.each do |a|
        begin
          return a if canonical_name(a)
        rescue
          # Ignore it and try the next one
        end
      end

      return aliases[-1]
    end

Given a list of aliases for the same host, returns the ‘best’ one. The shortest alias which is not an IP address is preferred. Otherwise, IP addresses are looked up via DNS and the fully qualified domain name of the first one to successfully resolve is returned.

[Source]

# File lib/sshmenu.rb, line 3007
    def best_alias(aliases)

      numeric_addr = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/

      # Return shortest host or domain name if there is one
      names = aliases.find_all { |h| !h.match(numeric_addr) }
      return names.sort { |a,b| a.length <=> b.length }[0] if names.length > 0

      # Otherwise, try and resolve an address to a name
      aliases.each do |a|
        begin
          return canonical_name(a)
        rescue
          # Ignore it and try the next one
        end
      end

      return aliases[0]
    end

Used to contruct the dialog box which will have its body contents replaced at each step.

[Source]

# File lib/sshmenu.rb, line 2907
    def build_dialog
      dialog = Gtk::Dialog.new(
        "Initial Setup",
        nil,
        Gtk::Dialog::MODAL | Gtk::Dialog::NO_SEPARATOR,
        [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_REJECT]
      )

      dialog.default_response = Gtk::Dialog::RESPONSE_ACCEPT

      table = Gtk::Table.new(2, 2, false)
      table.border_width = 0

      bgcolour = dialog.style.bg(Gtk::STATE_SELECTED)

      table.attach(
        Gtk::EventBox.new.modify_bg(Gtk::STATE_NORMAL, bgcolour).add(wizard_icon),
        0, 1, 0, 1, Gtk::FILL, Gtk::FILL, 0, 0
      )

      title = '<span size="xx-large" weight="heavy" foreground="white">' +
              ' SSH Menu: Initial Setup</span>'
      table.attach(
        Gtk::EventBox.new.modify_bg(Gtk::STATE_NORMAL, bgcolour).add(
          Gtk::Label.new(title).set_use_markup(true).set_width_chars(48).set_xalign(0)
        ),
        1, 2, 0, 1, Gtk::EXPAND|Gtk::FILL, Gtk::FILL, 0, 0
      )

      table.attach(
        Gtk::EventBox.new.modify_bg(Gtk::STATE_NORMAL, bgcolour).add(
          Gtk::Label.new("\n\n\n\n\n\n\n\n\n\n")
        ),
        0, 1, 1, 2, Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0
      )

      @body = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_NONE)
      table.attach(@body, 1, 2, 1, 2, Gtk::FILL, 0, 20, 20)

      dialog.vbox.add(table)

      dialog.show_all

      return dialog
    end

Given an address, returns the fully qualified domain name. May raise a ‘host not found’ exception

[Source]

# File lib/sshmenu.rb, line 3048
    def canonical_name(addr)
      inet = Socket.gethostbyname(addr)
      info = Socket.gethostbyaddr(inet[3])
      return info[0]
    end

This routine does the work of processing the known hosts file and updating the progress bar. It is an ‘idle handler’ so it is called multiple times until it returns false to indicate no further work remains. Calls list_host_aliases on the first invocation to get a list of raw host aliases. Each subsequent invocation processes one raw host entry to resolve names and addresses (for duplicate filtering).

[Source]

# File lib/sshmenu.rb, line 2882
    def import_tick(dialog, progress)
      if @raw_hosts.nil?
        @raw_hosts = list_host_aliases
        @curr_host = 0
        progress.fraction = (@curr_host + 1.0) / (@raw_hosts.length + 1.0)
        return true
      end

      if @curr_host >= @raw_hosts.length
        setup_step_three(@raw_hosts.length)
        return false  # no more work to do
      end

      aliases = @raw_hosts[@curr_host]
      @curr_host += 1
      name = best_alias(aliases)
      addr = best_address(name, *aliases)
      @imported[name] = addr
      progress.fraction = (@curr_host + 1.0) / (@raw_hosts.length + 1.0)
      return true
    end

Reads the known hosts file, skipping hashed host entries and collecting hostnames from the remaining entries - returns a list of hostnames.

[Source]

# File lib/sshmenu.rb, line 2987
    def list_host_aliases
      hosts = {}
      @known_hosts_file.each_line do |line|
        if line =~ /^[|]\d+[|]/
          @hashed_hosts_skipped += 1
        elsif line =~ /^([^|]\S*)\s+ssh-\S+\s+(\S+)/
          aliases, key = $1, $2
          hosts[key] ||= []
          aliases.split(/,/).each { |h| hosts[key] << h }
        end
      end

      return hosts.values
    end

Creates a dialog by calling build_dialog. Populates the body of the dialog by calling setup_step_one and awaits user interaction. Replaces the body of the dialog by calling setup_step_two and repeats. Returns true if the process was completed or false if the user pressed cancel.

[Source]

# File lib/sshmenu.rb, line 2763
    def run_wizard
      @dialog  = build_dialog
      setup_step_one
      if @dialog.run != Gtk::Dialog::RESPONSE_ACCEPT
        @dialog.destroy
        return false
      end

      setup_step_two
      if @dialog.run != Gtk::Dialog::RESPONSE_ACCEPT
        @dialog.destroy
        return false
      end

      @dialog.destroy
      return true
    end

Replaces the body of the wizard dialog box with the supplied widget.

[Source]

# File lib/sshmenu.rb, line 2857
    def set_body(widget)
      @body.each { |w| @body.remove(w) }
      @body.add(widget)
      @body.show_all
    end

Replaces the dialog‘s button box contents with the supplied list of buttons. Each button is represented as a two element array. The first element is the button text label and the second is the response code the button should generate.

[Source]

# File lib/sshmenu.rb, line 2868
    def set_buttons(*buttons)
      bbox = @dialog.action_area
      bbox.each { |w| bbox.remove(w) }
      buttons.each { |b| @dialog.add_button(*b) }
      @dialog.default_response = buttons.last[1]
    end

Sets up an introductory message describing the function of the wizard and provides Next and Cancel buttons (cleverly disguised as ‘Automatic Setup’ and ‘Manual Setup’ buttons respectively).

[Source]

# File lib/sshmenu.rb, line 2785
    def setup_step_one

      set_body(
        Gtk::Label.new(
          "The initial menu options can be created automatically\n" +
          "from your list of SSH known hosts.\n\n" +
          "Or, if you prefer, you can set up the menu manually."
        ).set_xalign(0)
      )

      set_buttons(
        ["Manual Setup",    Gtk::Dialog::RESPONSE_REJECT],
        ["Automatic Setup", Gtk::Dialog::RESPONSE_ACCEPT]
      )
    end

Called from import_tick when all the known_hosts entries have been processed.

[Source]

# File lib/sshmenu.rb, line 2832
    def setup_step_three(count)
      vbox = Gtk::VBox.new
      vbox.spacing = 10

      text = "#{count} host#{count == 1 ? '' : 's'} imported"
      if @hashed_hosts_skipped > 0
        text = text + " (#{@hashed_hosts_skipped} hashed hostnames were skipped)"
      end
      vbox.pack_start(
        Gtk::Label.new(text).set_xalign(0)
      )

      progress = Gtk::ProgressBar.new
      progress.fraction = 1.0
      vbox.pack_start(progress)
      set_body(vbox)

      set_buttons(
        ["Cancel", Gtk::Dialog::RESPONSE_REJECT],
        ["Finish", Gtk::Dialog::RESPONSE_ACCEPT]
      )
    end

Sets up a progress bar and registers import_tick as an ‘idle’ handler to do the work. The user can ‘Cancel’ at any time and can click ‘Finish’ once import_tick has processed the whole known_hosts file.

[Source]

# File lib/sshmenu.rb, line 2805
    def setup_step_two

      vbox = Gtk::VBox.new
      vbox.spacing = 10

      vbox.pack_start(
        Gtk::Label.new( "Importing known hosts.  Please wait ...").set_xalign(0)
      )

      progress = Gtk::ProgressBar.new
      vbox.pack_start(progress)
      set_body(vbox)

      set_buttons(
        ["Cancel", Gtk::Dialog::RESPONSE_REJECT],
        ["Finish", Gtk::Dialog::RESPONSE_ACCEPT]
      )

      @dialog.set_response_sensitive(Gtk::Dialog::RESPONSE_ACCEPT, false)

      @raw_hosts = nil
      Gtk.idle_add { import_tick(@dialog, progress) }
    end

Returns a Gtk::Image being the SSHMenu logo.

[Source]

# File lib/sshmenu.rb, line 2955
    def wizard_icon
      loader = Gdk::PixbufLoader.new
      loader.write(Base64.decode64("iVBORw0KGgoAAAANSUhEUgAAAEQAAABACAMAAACUXCGWAAAAw1BMVEXIAAAcHhslKCUrLSoyNTI3\nODA4Ozg7OzQ/QD47Qz9DQTpDREJISkdMSj1LSkNRUEhQUk9VUkVRWVVWWFVbWExZWVFeYF1aYl5h\nYFhjYFNhaWVqZ1pmaGVpaGBwb2dzb2JucG17dmN3dm51d3R7d2p8fnuAf3eHg3aJhHGGh4SJiICN\nj4yRkIeWk4WYmI+dn5yloZOjoZmuqpytq6OqrKmzsam6ubHEw7vOzcXY187W2NXe3dTd39zo6ebv\n8e79//sAAQCpFywPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAAC4cAAAuHAZNA\nh1MAAAAHdElNRQfVDAsBLCiRkORfAAAC1klEQVRYw+3WbVOiUBTA8ZV7QwrUuxCIBSyB0opIGYLG\natzv/6n2HASz2uH6olc7/mf02kz8PDwM8uPHpUuXviF+Zp3Gfv/2sX3TrnzdbrebzaYoijxLuhT+\n9or9OQn/3rbb53mWrVarNBIjn4ja2B5myGsjDUQI/vOxHMuyw/enSRLHUZSegRSig5qliRDJua5b\n4/Gd49zd1W9jzNB1xjRVkSnPk8QTI9r80AwKw9D3fQeyIMMwCM+SuBvZbwEZz1qiMVoEDEZ5FguR\nbcbZrC38Mggj/CWOXDFihe/EJwMneY7OQfRmY7/dk4NhGQwiiNjdyGaz4o4fWnAmVM2wLKYqiqIZ\nTFGYocmyxq4ACc5AdN9Xn3ZVVS4UpVlptVOYXJUyThIEZieyK+pJ/HW19sOiWrerVJVUI1VJ4Jgs\nA68bKYsi5brjlBWVCFmEzToDpEd6iEh86YmQHBC4Tn3YifWCSc1KpKquJBpM4rojAZInOAml1ny9\nr+ZSs/aqnazKiJAzkAxuOLblzJ8Iha8vYSUS7gtsr0jNJLbdjWyzLOa6ZT1VBR7dJ1wdWBFR8cBq\nEn+07WE3slrFfGxZ8qI+tZQ2K8GzQ+s3QEwBkq4izvAakymVVVavVNFUSlVNgU8qTGIKkE26jPjY\nOIkd0jAVoojcdCJFArc+zfjYBwUmGY26EbhrBdz8ShwNhfCpEIkfPX7N2Md9ORC1oUiADPudSIYI\n+9yJgchQhESPLleuIfVaVeCFH2U8QwSvOknq9fjDcNCNrKZTW/ST8TAQIOl0unw59nxoWff4u0mM\n/DrtHppMJubt7e1PbNDWjSRfiUlLIGJ67kiM3J80aYh3YxAMBjdCJH4n3H8YsHkf37svNnc4OjbE\nbtr62FVTnwset0IvCKJoHs8XWP2DGkWzMPA8z3GxG8HjFjLWEKYxTdt1fNgONrJt2zRxsGYmwr/l\n0e/ygHzp0v/WX7AH3cFL3lx/AAAAAElFTkSuQmCC\n"
))
    loader.close
    return Gtk::Image.new(loader.pixbuf)
  end

[Validate]