ShogunPanda/pincerna

View on GitHub
lib/pincerna/ip.rb

Summary

Maintainability
A
1 hr
Test Coverage
# encoding: utf-8
#
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
#

module Pincerna
  # Shows the IP addresses of all network interfaces.
  class Ip < Base
    # The expression to match.
    MATCHER = /^(?<all>.*)$/i

    # The icon to show for each feedback item.
    ICON = Pincerna::Base::ROOT + "/images/network.png"

    # The URL of the webservice.
    URL = "http://api.externalip.net/ip"

    # Shows the IP addresses of all network interfaces.
    #
    # @param query [Array] A query to match against interfaces names.
    # @return [Array] A list of items to process.
    def perform_filtering(query)    
      @interface_filter ||= query.empty? ? /.*/ : /#{query}/i

      # Get local addresses
      rv = get_local_addresses

      # Sort interfaces and address, IPv4 first then IPv6
      rv.sort! {|left, right|
        cmp = left[:interface] <=> right[:interface] # Interface name first
        cmp = compare_ip_classes(left[:address], right[:address]) if cmp == 0 # Now IPv4 first then IPv6
        cmp = compare_ip_addresses(left[:address], right[:address]) if cmp == 0 # Finally addresses
        cmp
      }

      # Add public address
      rv = rv.insert(0, get_public_address) if @interface_filter.match("public")

      rv
    end

    # Processes items to obtain feedback items.
    #
    # @param results [Array] The items to process.
    # @return [Array] The feedback items.
    def process_results(results)
      results.map do |result|
        title = "#{result[:interface] ? result[:interface] : "Public"} IP: #{result[:address]}"
        {title: title, arg: result[:address], subtitle: "Action this item to copy the IP on the clipboard.", icon: ICON}
      end
    end

    # Gets a list of local IP addresses.
    #
    # @return [Array] A list of IPs data.
    def get_local_addresses
      rv = []
      names = get_interfaces_names

      # Split by interfaces
      interfaces = execute_command("/sbin/ifconfig").split(/(^\S+:\s+)/)
      interfaces.shift # Discard first whitespace

      # For each interface
      interfaces.each_slice(2) do |interface, configuration|
        # See if matches the query and then replace public name
        interface = interface.gsub(/\s*(.+):\s*/, "\\1")
        name = names[interface] ? "#{names[interface]} (#{interface})" : interface
        next if !@interface_filter.match(name)

        # Get addresses
        addresses = StringScanner.new(configuration)
        while addresses.scan_until(/inet(6?)\s/) do
          rv << {interface: name, address: addresses.scan(/\S+/)}
        end
      end

      rv
    end

    # Gets the public IP address for this machine.
    #
    # @return [Hash] The public IP address data.
    def get_public_address
      {interface: nil, address: fetch_remote_resource(URL, {}, false)}
    end

    # Compares two IP classes, giving higher priority to IPv4.
    #
    # @param left [String] The first IP to compare.
    # @param right [String] The second IP to compare.
    # @return [Fixnum] The result of the comparison.
    def compare_ip_classes(left, right)
      (left.index(":") ? 1 : 0) <=> (right.index(":") ? 1 : 0)
    end

    # Compares to IP addresses, giving higher priority to local address such as 127.0.0.1.
    #
    # @param left [String] The first IP to compare.
    # @param right [String] The second IP to compare.
    # @return [Fixnum] The result of the comparison.
    def compare_ip_addresses(left, right)
      higher_priority = ["::1", "127.0.0.1", "10.0.0.1"]
      cmp = (higher_priority.include?(left) ? 0 : 1) <=> (higher_priority.include?(right) ? 0 : 1)
      cmp = left <=> right if cmp == 0
      cmp
    end

    # Gets a hash with pair of interfaces and their human names.
    #
    # @return [Hash] The hash with interfaces' name.
    def get_interfaces_names
      rv = {"lo0" => "Loopback"}

      names = execute_command("/usr/sbin/networksetup", "-listallhardwareports").split(/\n\n/)
      names.shift # Discard first whitespace
      
      names.each do |port|
        port = StringScanner.new(port)
        name = nil
        interface = nil
        
        if port.scan_until(/Hardware Port: /) then
          name = port.scan_until(/\n/).strip
          interface = port.scan_until(/\n/).strip if port.scan_until(/Device: /)
        end

        rv[interface] = name if name && interface
      end

      rv
    end
  end
end