# encoding: utf-8 # # This file is part of the akaer gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>. # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php. # # A small utility to add aliases to network interfaces. module Akaer # The main Akaer application. class Application # The {Configuration Configuration} of this application. attr_reader :config # The Mamertes command. attr_reader :command # The logger for this application. attr_accessor :logger # Creates a new application. # # @param command [Mamertes::Command] The current Mamertes command. def initialize(command) @command = command application = @command.application # Setup logger Bovem::Logger.start_time = Time.now @logger = Bovem::Logger.create(Bovem::Logger.get_real_file(application.options["log-file"].value) || Bovem::Logger.default_file, Logger::INFO) # Open configuration begin overrides = { :interface => application.options["interface"].value, :addresses => application.options["addresses"].value, :start_address => application.options["start-address"].value, :aliases => application.options["aliases"].value, :add_command => application.options["add-command"].value, :remove_command => application.options["remove-command"].value, :log_file => application.options["log-file"].value, :log_level => application.options["log-level"].value, :dry_run => application.options["dry-run"].value, :quiet => application.options["quiet"].value }.reject {|k,v| v.nil? } @config = Akaer::Configuration.new(application.options["configuration"].value, overrides, @logger) @logger = nil @logger = self.get_logger rescue Bovem::Errors::InvalidConfiguration => e @logger ? @logger.fatal(e.message) : Bovem::Logger.create("STDERR").fatal("Cannot log to {mark=bright}#{config.log_file}{/mark}. Exiting...") raise ::SystemExit end self end # Checks if we are running on MacOS X. # # System services are only available on that platform. # # @return [Boolean] `true` if the current platform is MacOS X, `false` otherwise. def is_osx? ::Config::CONFIG['host_os'] =~ /^darwin/ end # Checks if and address is a valid IPv4 address. # # @param address [String] The address to check. # @return [Boolean] `true` if the address is a valid IPv4 address, `false` otherwise. def is_ipv4?(address) address = address.ensure_string mo = /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/.match(address) rv = (mo && mo.captures.all? {|i| i.to_i < 256}) ? true : false end # Checks if and address is a valid IPv6 address. # # @param address [String] The address to check. # @return [Boolean] `true` if the address is a valid IPv6 address, `false` otherwise. def is_ipv6?(address) address = address.ensure_string rv = catch(:valid) do # IPv6 (normal) throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*\Z/ =~ address throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ address throw(:valid, true) if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ address # IPv6 (IPv4 compat) throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:/ =~ address && self.is_ipv4?($') throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ address && self.is_ipv4?($') throw(:valid, true) if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ address && self.is_ipv4?($') false end end # Pads a number to make it print friendly. # # @param num [Fixnum] The number to pad. # @param len [Fixnum] The minimum length of the padded string. # @return [String] The padded number. def pad_number(num, len = nil) num.to_integer.to_s.rjust([len.to_integer, 2].max, "0") end # Gets the current logger of the application. # # @return [Logger] The current logger of the application. def get_logger @logger ||= Bovem::Logger.create(Bovem::Logger.default_file, @config.log_level, @log_formatter) end # Gets the path for the launch agent file. # # @param name [String] The base name for the agent. # @return [String] The path for the launch agent file. def launch_agent_path(name = "it.cowtech.akaer") ENV["HOME"] + "/Library/LaunchAgents/#{name}.plist" end # Executes a shell command. # # @param command [String] The command to execute. # @return [Boolean] `true` if command succeeded, `false` otherwise. def execute_command(command) Kernel.system(command) end # Computes the list of address to manage. # # @param type [Symbol] The type of addresses to consider. Valid values are `:ipv4`, `:ipv6`, otherwise all addresses are considered. # @return [Array] The list of addresses to add or remove from the interface. def compute_addresses(type = :all) rv = [] if self.config.addresses.present? # We have an explicit list rv = self.config.addresses # Now filter the addresses filters = [type != :ipv6 ? :ipv4 : nil, type != :ipv4 ? :ipv6 : nil].compact rv = rv.select {|address| filters.any? {|filter| self.send("is_#{filter}?", address) } }.compact.uniq else begin ip = IPAddr.new(self.config.start_address.ensure_string) raise ArgumentError if (type == :ipv4 && !ip.ipv4?) || (type == :ipv6 && !ip.ipv6?) (self.config.aliases > 0 ? self.config.aliases : 5).times do rv << ip.to_s ip = ip.succ end rescue ArgumentError end end rv end # Adds or removes an alias from the interface. # # @param type [Symbol] The operation to execute. Can be `:add` or `:remove`. # @param address [String] The address to manage. # @return [Boolean] `true` if operation succedeed, `false` otherwise. def manage(type, address) rv = true # Compute the command command = (type == :remove) ? self.config.remove_command : self.config.add_command # Interpolate command = command.gsub("@INTERFACE@", self.config.interface).gsub("@ALIAS@", address) + " > /dev/null 2>&1" # Compute the prefix @addresses ||= self.compute_addresses length = self.pad_number(@addresses.length) index = (@addresses.index(address) || 0) + 1 prefix = "{mark=blue}[{mark=bright white}#{self.pad_number(index, length.length)}{mark=reset blue}/{/mark}#{length}{/mark}]{/mark}" # Now execute if !self.config.dry_run then @logger.info(@command.application.console.replace_markers("#{prefix} {mark=bright}#{(type == :remove ? "Removing" : "Adding")}{/mark} address {mark=bright}#{address}{/mark} #{type != :remove ? "to" : "from"} interface {mark=bright}#{self.config.interface}{/mark}...")) if !self.config.quiet rv = self.execute_command(command) @logger.error(@command.application.console.replace_markers("Cannot {mark=bright}#{(type == :remove ? "remove" : "add")}{/mark} address {mark=bright}#{address}{/mark} #{type != :remove ? "to" : "from"} interface {mark=bright}#{self.config.interface}{/mark}.")) if !rv else @logger.info(@command.application.console.replace_markers("#{prefix} I will {mark=bright}#{(type == :remove ? "remove" : "add")}{/mark} address {mark=bright}#{address}{/mark} #{type != :remove ? "to" : "from"} interface {mark=bright}#{self.config.interface}{/mark}.")) if !self.config.quiet end rv end # Adds aliases to the interface. # # @return [Boolean] `true` if action succedeed, `false` otherwise. def action_add addresses = self.compute_addresses if addresses.present? then # Now, for every address, call the command addresses.all? {|address| self.manage(:add, address) } else @logger.error("No valid addresses to add to the interface found.") if !self.config.quiet end end # Removes aliases from the interface. # # @return [Boolean] `true` if action succedeed, `false` otherwise. def action_remove addresses = self.compute_addresses if addresses.present? then # Now, for every address, call the command addresses.all? {|address| self.manage(:remove, address) } else @logger.error("No valid addresses to remove from the interface found.") if !self.config.quiet end end # Installs the application into the autolaunch. # # @return [Boolean] `true` if action succedeed, `false` otherwise. def action_install logger = get_logger if !self.is_osx? then logger.fatal("Install akaer on autolaunch is only available on MacOSX.") if !self.config.quiet return false end launch_agent = self.launch_agent_path begin logger.info("Creating the launch agent in {mark=bright}#{launch_agent}{/mark} ...") if !self.config.quiet args = $ARGV ? $ARGV[0, $ARGV.length - 1] : [] plist = {"KeepAlive" => false, "Label" => "it.cowtech.akaer", "Program" => (::Pathname.new(Dir.pwd) + $0).to_s, "ProgramArguments" => args, "RunAtLoad" => true} ::File.open(launch_agent, "w") {|f| f.write(plist.to_json) f.flush } self.execute_command("plutil -convert binary1 \"#{launch_agent}\"") rescue => e logger.error("Cannot create the launch agent.") if !self.config.quiet return false end begin logger.info("Loading the launch agent ...") if !self.config.quiet self.execute_command("launchctl load -w \"#{launch_agent}\" > /dev/null 2>&1") rescue => e logger.error("Cannot load the launch agent.") if !self.config.quiet return false end true end # Uninstalls the application from the autolaunch. # # @return [Boolean] `true` if action succedeed, `false` otherwise. def action_uninstall logger = self.get_logger if !self.is_osx? then logger.fatal("Install akaer on autolaunch is only available on MacOSX.") if !self.config.quiet return false end launch_agent = self.launch_agent_path # Unload the launch agent. begin self.execute_command("launchctl unload -w \"#{launch_agent}\" > /dev/null 2>&1") rescue => e logger.warn("Cannot unload the launch agent.") if !self.config.quiet end # Delete the launch agent. begin logger.info("Deleting the launch agent #{launch_agent} ...") ::File.delete(launch_agent) rescue => e logger.warn("Cannot delete the launch agent.") if !self.config.quiet return false end true end # Returns a unique (singleton) instance of the application. # # @param command [Mamertes::Command] The current Mamertes command. # @param force [Boolean] If to force recreation of the instance. # @return [Application] The unique (singleton) instance of the application. def self.instance(command, force = false) @instance = nil if force @instance ||= Akaer::Application.new(command) end end end