lib/devdnsd/aliases.rb
# encoding: utf-8
#
# This file is part of the devdnsd 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.
#
# A small DNS server to enable local .dev domain resolution.
module DevDNSd
# Methods to handle interfaces aliases.
module Aliases
extend ActiveSupport::Concern
# Manages aliases.
#
# @param operation [Symbol] The type of operation. Can be `:add` or `:remove`.
# @param message [String] The message to show if no addresses are found.
# @param options [Hash] The options provided by the user.
# @return [Boolean] `true` if operation succeeded, `false` otherwise.
def manage_aliases(operation, message, options)
config = self.config
options.each { |k, v| config.send("#{k}=", v) if config.respond_to?("#{k}=") }
addresses = compute_addresses
if addresses.present?
# Now, for every address, call the command
addresses.all? { |address| manage_address(operation, address, options[:dry_run]) }
else
@logger.error(message)
false
end
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.
# @param dry_run [Boolean] If only show which modifications will be done.
# @return [Boolean] `true` if operation succeeded, `false` otherwise.
def manage_address(type, address, dry_run = false)
rv, command, prefix = setup_management(type, address)
# Now execute
if rv
if !dry_run
execute_manage(command, prefix, type, address, config)
else
log_management(:dry_run, prefix, type, i18n.remove, i18n.add, address, config)
end
end
rv
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)
config = self.config
config.addresses.present? ? filter_addresses(config, type) : generate_addresses(config, type)
end
# Checks if an 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 ipv4?(address)
address = address.ensure_string
mo = /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/.match(address)
(mo && mo.captures.all? { |i| i.to_i < 256 }) ? true : false
end
alias_method :is_ipv4?, :ipv4?
# Checks if an 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 ipv6?(address)
address = address.ensure_string
catch(:valid) do
# IPv6 (normal)
check_normal_ipv6(address)
# IPv6 (IPv4 compat)
check_compat_ipv6(address)
false
end
end
private
# :nodoc:
def setup_management(type, address)
@addresses ||= compute_addresses
length = @addresses.length
length_s = length.to_s.length
progress = ((@addresses.index(address) || 0) + 1).indexize(length: length_s)
message = "{mark=blue}[{mark=bright white}#{progress}{mark=reset blue}/{/mark}#{length}{/mark}]{/mark}"
[true, build_command(type, address), message]
rescue ArgumentError
[false]
end
# :nodoc:
def filter_addresses(config, type)
filters = [:ipv4, :ipv6].select { |i| type == i || type == :all }.compact
config.addresses.select { |address| filters.any? { |filter| send("#{filter}?", address) } }.compact.uniq
end
# :nodoc:
def generate_addresses(config, type)
ip = IPAddr.new(config.start_address.ensure_string)
raise ArgumentError if type != :all && !ip.send("#{type}?")
Array.new([config.aliases, 1].max) do |_|
current = ip
ip = ip.succ
current
end
rescue ArgumentError
[]
end
# :nodoc:
def build_command(type, address)
template = config.send((type == :remove) ? :remove_command : :add_command)
Mustache.render(template, {interface: config.interface, address: address.to_s}) + " > /dev/null 2>&1"
end
# :nodoc:
def execute_manage(command, prefix, type, address, config)
rv = execute_command(command)
log_management(:run, prefix, type, i18n.removing, i18n.adding, address, config)
log_management_error(config, address, manage_labels(type)) unless rv
rv
end
# :nodoc:
def manage_labels(type)
type == :remove ? [i18n.remove, i18n.from] : [i18n.add, i18n.to]
end
# :nodoc:
def log_management_error(config, address, labels)
@logger.error(replace_markers(i18n.general_error(labels[0], address, labels[1], config.interface)))
end
# :nodoc:
def log_management(message, prefix, type, remove_label, add_label, address, config)
labels = (type == :remove ? [remove_label, i18n.from] : [add_label, i18n.to])
@logger.info(replace_markers(i18n.send(message, prefix, labels[0], address, labels[1], config.interface)))
end
# :nodoc:
def check_compat_ipv6(address)
throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:/ =~ address && 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 && ipv4?($')
throw(:valid, true) if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ address && ipv4?($')
end
# :nodoc:
def check_normal_ipv6(address)
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
end
end
end