rapid7/metasploit-framework

View on GitHub
lib/rex/proto/dns/static_hostnames.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# -*- coding: binary -*-

require 'rex/socket'
require 'forwardable'

module Rex
module Proto
module DNS
  ##
  # This class manages statically defined hostnames for DNS resolution where each name is a mapping to an IPv4 and or
  # an IPv6 address. A single hostname can only map to one address of each family.
  ##
  class StaticHostnames
    extend Forwardable

    def_delegators :@hostnames, :each, :each_with_index, :length, :empty?, :sort_by

    # @param [Hash<String, IPAddr>] hostnames The hostnames to IP address mappings to initialize with.
    def initialize(hostnames: nil)
      @hostnames = {}
      if hostnames
        hostnames.each do |hostname, ip_address|
          add(hostname, ip_address)
        end
      end
    end

    # Locate and parse a hosts file on the system. Only the first hostname to IP address definition is used which
    # replicates the behavior of /etc/hosts on Linux. Loaded definitions are merged with existing definitions.
    def parse_hosts_file
      path = %w[
        %WINDIR%\system32\drivers\etc\hosts
        /etc/hosts
        /data/data/com.termux/files/usr/etc/hosts
      ].find do |path|
        path = File.expand_path(path)
        File.file?(path) && File.readable?(path)
      end
      return unless path

      path = File.expand_path(path)
      ::IO.foreach(path) do |line|
        words = line.split
        next unless words.length > 1 && Rex::Socket.is_ip_addr?(words.first)

        ip_address = IPAddr.new(words.shift)
        words.each do |hostname|
          add(hostname, ip_address)
        end
      end
    end

    # Get an IP address of the specified type for the hostname. Only the first address is returned in cases where
    # multiple addresses are defined.
    #
    # @param [String] hostname The hostname to retrieve an address for.
    # @param [Integer] type The family of address to return represented as a DNS type (either A or AAAA).
    # @return Returns the IP address if it was found, otherwise nil.
    # @rtype [IPAddr, nil]
    def get1(hostname, type = Dnsruby::Types::A)
      get(hostname, type).first
    end

    # Get all IP addresses of the specified type for the hostname.
    #
    # @param [String] hostname The hostname to retrieve an address for.
    # @param [Integer] type The family of address to return represented as a DNS type (either A or AAAA).
    # @return Returns an array of IP addresses.
    # @rtype [Array<IPAddr>]
    def get(hostname, type = Dnsruby::Types::A)
      hostname = hostname.downcase
      @hostnames.fetch(hostname, {}).fetch(type, []).dup
    end

    # Add an IP address for the specified hostname.
    #
    # @param [String] hostname The hostname whose IP address is being defined.
    # @param [IPAddr, String] ip_address The IP address that is being defined for the hostname. If this value is a
    #   string, it will be converted to an IPAddr instance.
    def add(hostname, ip_address)
      unless self.class.is_valid_hostname?(hostname)
        # it is important to validate the hostname because assumptions about what characters it may contain are made
        # when saving and loading it from the configuration
        raise ::ArgumentError.new("Invalid hostname: #{hostname}")
      end

      ip_address = IPAddr.new(ip_address) if ip_address.is_a?(String) && Rex::Socket.is_ip_addr?(ip_address)

      hostname = hostname.downcase.delete_suffix('.')
      this_host = @hostnames.fetch(hostname, {})
      if ip_address.family == ::Socket::AF_INET
        type = Dnsruby::Types::A
      else
        type = Dnsruby::Types::AAAA
      end
      this_type = this_host.fetch(type, [])
      this_type << ip_address unless this_type.include?(ip_address)
      this_host[type] = this_type
      @hostnames[hostname] = this_host
      nil
    end

    # Delete an IP address for the specified hostname.
    #
    # @param [String] hostname The hostname whose IP address is being undefined.
    # @param [IPAddr, String] ip_address The IP address that is being undefined. If this value is a string, it will be
    #   converted to an IPAddr instance.
    def delete(hostname, ip_address)
      ip_address = IPAddr.new(ip_address) if ip_address.is_a?(String) && Rex::Socket.is_ip_addr?(ip_address)
      if ip_address.family == ::Socket::AF_INET
        type = Dnsruby::Types::A
      else
        type = Dnsruby::Types::AAAA
      end

      hostname = hostname.downcase
      this_host = @hostnames.fetch(hostname, {})
      this_type = this_host.fetch(type, [])
      this_type.delete(ip_address)
      if this_type.empty?
        this_host.delete(type)
      else
        this_host[type] = this_type
      end
      if this_host.empty?
        @hostnames.delete(hostname)
      else
        @hostnames[hostname] = this_host
      end

      nil
    end

    # Delete all hostname to IP address definitions.
    def flush
      @hostnames.clear
    end

    def self.is_valid_hostname?(name)
      # check if it appears to be a fully qualified domain name, e.g. www.metasploit.com
      return true if Rex::Socket.is_name?(name)

      # check if it appears to at least be a valid hostname, e.g. localhost
      return true if (name =~ /^([a-z0-9][a-z0-9\-]{0,61})?[a-z0-9]$/i) && (name =~ /\s/).nil?

      false
    end
  end
end
end
end