ronin-rb/ronin-support

View on GitHub
lib/ronin/support/network/ip_range/glob.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true
#
# Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-support is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-support is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-support.  If not, see <https://www.gnu.org/licenses/>.
#

require 'combinatorics/list_comprehension'

module Ronin
  module Support
    module Network
      class IPRange
        #
        # Represents an IP-glob range.
        #
        # ## Examples
        #
        #     ip_range = IPRange::Glob.new('10.0.1-3.*/24')
        #     ip_range.each { |ip| puts ip }
        #     # 10.0.1.0
        #     # 10.0.1.1
        #     # ...
        #     # 10.0.1.254
        #     # 10.0.1.255
        #     # ...
        #     # 10.0.2.0
        #     # 10.0.2.1
        #     # ...
        #     # 10.0.2.254
        #     # 10.0.2.255
        #     # ...
        #     # 10.0.3.0
        #     # 10.0.3.1
        #     # ...
        #     # 10.0.3.254
        #     # 10.0.3.255
        #
        # @api public
        #
        # @since 1.0.0
        #
        class Glob

          include Enumerable

          # The IP glob string.
          #
          # @return [String]
          attr_reader :string

          #
          # Initializes and parses the IP-glob range.
          #
          # @param [String] string
          #   The IP-glob string to parse.
          #
          def initialize(string)
            @string = string

            if @string.include?(':') # IPv6
              @version   = 6
              @base      = 16
              @formatter = method(:format_ipv6_address)

              separator   = ':'
              octet_range = (0..0xffff)
            else # IPv4
              @version   = 4
              @base      = 10
              @formatter = method(:format_ipv4_address)

              separator   = '.'
              octet_range = (0..255)
            end

            @ranges = @string.split(separator).map do |segment|
              if    segment == '*'        then octet_range
              elsif segment.include?(',') then parse_list(segment)
              elsif segment.include?('-') then parse_range(segment)
              else                             [segment]
              end
            end
          end

          #
          # Alias for {#initialize new}.
          #
          # @param [String] string
          #   The IP-glob string to parse.
          #
          # @return [Glob]
          #   The parsed IP-glob range.
          #
          # @see #initialize
          #
          def self.parse(string)
            new(string)
          end

          #
          # Enumerates over the IP-glob range.
          #
          # @param [String] string
          #   The IP-glob string to parse and enumerate over.
          #
          # @yield [ip]
          #   The block which will be passed each IP address contained within
          #   the IP address range.
          #
          # @yieldparam [String] ip
          #   An IP address within the IP address range.
          #
          # @return [self]
          #
          # @example Enumerate through a IPv4 glob range:
          #   IPRange::Glob.each('10.0.1-3.*') { |ip| puts ip }
          #   # 10.0.1.0
          #   # 10.0.1.1
          #   # ...
          #   # 10.0.1.254
          #   # 10.0.1.255
          #   # ...
          #   # 10.0.2.0
          #   # 10.0.2.1
          #   # ...
          #   # 10.0.2.254
          #   # 10.0.2.255
          #   # ...
          #   # 10.0.3.0
          #   # 10.0.3.1
          #   # ...
          #   # 10.0.3.254
          #   # 10.0.3.255
          #
          # @example Enumerate through a globbed IPv6 range:
          #   IPRange::Glob.each('::ff::02-0a::c3') { |ip| puts ip }
          #
          def self.each(string,&block)
            new(string).each(&block)
          end

          #
          # Determines if the IP-glob range is IPv4.
          #
          # @return [Boolean]
          #
          def ipv4?
            @version == 4
          end

          #
          # Determines if the IP-glob range is IPv6.
          #
          # @return [Boolean]
          #
          def ipv6?
            @version == 6
          end

          #
          # Enumerates over the IP-glob range.
          #
          # @yield [ip]
          #   The block which will be passed each IP address contained within
          #   the IP address range.
          #
          # @yieldparam [String] ip
          #   An IP address within the IP address range.
          #
          # @return [self]
          #
          # @example Enumerate through a IPv4 glob range:
          #   ip_range = IPRange::Glob.new('10.0.1-3.*')
          #   ip_range.each { |ip| puts ip }
          #   # 10.0.1.0
          #   # 10.0.1.1
          #   # ...
          #   # 10.0.1.254
          #   # 10.0.1.255
          #   # ...
          #   # 10.0.2.0
          #   # 10.0.2.1
          #   # ...
          #   # 10.0.2.254
          #   # 10.0.2.255
          #   # ...
          #   # 10.0.3.0
          #   # 10.0.3.1
          #   # ...
          #   # 10.0.3.254
          #   # 10.0.3.255
          #
          # @example Enumerate through a globbed IPv6 range:
          #   ip_range = IPRange::Glob.new('::ff::02-0a::c3')
          #   ip_range.each { |ip| puts ip }
          #
          def each
            return enum_for(__method__) unless block_given?

            # cycle through the address ranges
            @ranges.comprehension do |address|
              yield @formatter.call(address)
            end

            return self
          end

          #
          # Converts the IP-glob range back into a String.
          #
          # @return [String]
          #
          def to_s
            @string
          end

          #
          # Inspects the IP-glob range.
          #
          # @return [String]
          #
          def inspect
            "#<#{self.class}: #{@string}>"
          end

          private

          #
          # Parses a comma separated list of numbers or ranges (ex: `i,j,k-l`).
          #
          # @param [String] list
          #   The string to parse.
          #
          # @return [Array<Integer, Range>]
          #   The parsed list.
          #
          def parse_list(list)
            list.split(',').flat_map do |octet|
              if octet.include?('-')
                # i-j range
                parse_range(octet)
              else
                octet.to_i(@base)
              end
            end
          end

          #
          # Parses a range of numbers. (ex: `i-j`).
          #
          # @param [String] range
          #   The string to parse.
          #
          # @return [Range<Integer,Integer>]
          #   The parsed range.
          #
          def parse_range(range)
            start, stop = range.split('-',2)

            start = start.to_i(@base)
            stop  = stop.to_i(@base)

            (start..stop).to_a
          end

          #
          # Formats an IPv4 address.
          #
          # @param [Array<String, Integer>] parts
          #   The address parts to format.
          #
          # @return [String]
          #   The formatted IPv4 address.
          #
          def format_ipv4_address(parts)
            parts.join('.')
          end

          #
          # Formats an IPv6 address.
          #
          # @param [Array<String, Integer>] parts
          #   The address parts to format.
          #
          # @return [String]
          #   The formatted IPv6 address.
          #
          def format_ipv6_address(parts)
            parts.map { |part|
              case part
              when Integer then part.to_s(16)
              else              part
              end
            }.join(':')
          end

        end
      end
    end
  end
end