diff --git a/Gemfile.lock b/Gemfile.lock index 921b06b..8e04e86 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,6 @@ PATH remote: . specs: spf (0.1.1) - ruby-ip (~> 0.9.1) GEM remote: http://rubygems.org/ @@ -32,7 +31,6 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.3) - ruby-ip (0.9.3) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) diff --git a/lib/spf/eval.rb b/lib/spf/eval.rb index 06ab245..10a6444 100644 --- a/lib/spf/eval.rb +++ b/lib/spf/eval.rb @@ -1,5 +1,4 @@ # encoding: ASCII-8BIT -require 'ip' require 'resolv' require 'spf/error' diff --git a/lib/spf/macro_string.rb b/lib/spf/macro_string.rb index 946a8a2..74eaff3 100644 --- a/lib/spf/macro_string.rb +++ b/lib/spf/macro_string.rb @@ -78,10 +78,10 @@ def expand(context = nil) elsif char == 'i' # RFC 4408, 8.1/20, 8.1/21 ip_address = request.ip_address ip_address = SPF::Util.ipv6_address_to_ipv4(ip_address) if SPF::Util.ipv6_address_is_ipv4_mapped(ip_address) - if IP::V4 === ip_address - value = ip_address.to_addr - elsif IP::V6 === ip_address - value = ip_address.to_hex.upcase.split('').join('.') + if ip_address.ipv4? + value = ip_address.to_s + elsif ip_address.ipv6? + value = ip_address.to_i.to_s(16).rjust(32, '0').upcase.split('').join('.') else server.throw_result(:permerror, request, "Unexpected IP address version in request") end @@ -92,9 +92,9 @@ def expand(context = nil) rh_parts = nil reverse = nil elsif char == 'v' # RFC 4408, 8.1/6/7 - if IP::V4 === request.ip_address + if request.ip_address.ipv4? value = 'in-addr' - elsif IP::V6 === request.ip_address + elsif request.ip_address.ipv6? value = 'ip6' else # Unexpected IP address version. diff --git a/lib/spf/model.rb b/lib/spf/model.rb index dc61859..0c19082 100644 --- a/lib/spf/model.rb +++ b/lib/spf/model.rb @@ -1,18 +1,9 @@ # encoding: ASCII-8BIT -require 'ip' +require 'ipaddr' require 'spf/util' -class IP - def contains?(ip_address) - return ( - self.to_irange.first <= ip_address.to_i and - self.to_irange.last >= ip_address.to_i) - end -end - - class SPF::Record DEFAULT_QUALIFIER = '+'; end @@ -161,7 +152,7 @@ def parse_ipv4_network(required = false) self.parse_ipv4_address(required) self.parse_ipv4_prefix_length begin - @ip_network = IP.new("#{@ip_address}/#{@ipv4_prefix_length}") if @ip_address and @ipv4_prefix_length + @ip_network = IPAddr.new("#{@ip_address}/#{@ipv4_prefix_length}") if @ip_address and @ipv4_prefix_length rescue ArgumentError @ip_network = @ip_address end @@ -200,7 +191,7 @@ def parse_ipv6_network(required = false) self.parse_ipv6_address(required) self.parse_ipv6_prefix_length begin - @ip_network = IP.new("#{@ip_address}/#{@ipv6_prefix_length}") if @ip_address and @ipv6_prefix_length + @ip_network = IPAddr.new("#{@ip_address}/#{@ipv6_prefix_length}") if @ip_address and @ipv6_prefix_length rescue ArgumentError @ip_network = @ip_address end @@ -349,13 +340,13 @@ def match_in_domain(server, request, domain) rrs.each do |rr| if Resolv::DNS::Resource::IN::A === rr - network = IP.new("#{rr.address}/#{ipv4_prefix_length}") + network = IPAddr.new("#{rr.address}/#{ipv4_prefix_length}") @ip_netblocks << network - return true if network.contains?(request.ip_address) + return true if network.include?(request.ip_address) elsif Resolv::DNS::Resource::IN::AAAA === rr - network = IP.new("#{rr.address}/#{ipv6_prefix_length}") + network = IPAddr.new("#{rr.address}/#{ipv6_prefix_length}") @ip_netblocks << network - return true if network.contains?(request.ip_address_v6) + return true if network.include?(request.ip_address_v6) else # Unexpected RR type. # TODO: Generate debug info or ignore silently. @@ -466,11 +457,11 @@ class SPF::Mech::IP4 < SPF::Mech def parse_params(required = true) self.parse_ipv4_network(required) - if IP === @ip_network + if IPAddr === @ip_network @ip_netblocks << @ip_network - if @ip_network.respond_to?(:offset) && @ip_network.offset != 0 + if SPF::Util.ip_network_has_host_bits?(@ip_address, @ip_network) @errors << SPF::InvalidMechCIDRError.new( - "Invalid CIDR netblock - bits in host portion of address of #{@ip_network}" + "Invalid CIDR netblock - bits in host portion of address of #{self.params}" ) end end @@ -479,19 +470,20 @@ def parse_params(required = true) def params return nil unless @ip_network return @ip_network if String === @ip_network - result = @ip_network.to_addr - if @ip_network.pfxlen != @default_ipv4_prefix_length - result += "/#{@ip_network.pfxlen}" + # Render the mechanism's original, unmasked address. + result = IPAddr.new(@ip_address).to_s + if @ip_network.prefix != @default_ipv4_prefix_length + result += "/#{@ip_network.prefix}" end return result end def match(server, request, want_result = true) return false unless @ip_network - ip_network_v6 = IP::V4 === @ip_network ? + ip_network_v6 = (IPAddr === @ip_network and @ip_network.ipv4?) ? SPF::Util.ipv4_address_to_ipv6(@ip_network) : @ip_network - return ip_network_v6.contains?(request.ip_address_v6) + return ip_network_v6.include?(request.ip_address_v6) end end @@ -502,10 +494,10 @@ class SPF::Mech::IP6 < SPF::Mech def parse_params(required = true) self.parse_ipv6_network(required) - @ip_netblocks << @ip_network if IP === @ip_network - if @ip_network.respond_to?(:offset) && @ip_network.offset != 0 + @ip_netblocks << @ip_network if IPAddr === @ip_network + if SPF::Util.ip_network_has_host_bits?(@ip_address, @ip_network) @errors << SPF::InvalidMechCIDRError.new( - "Invalid CIDR netblock - bits in host portion of address of #{@ip_network}" + "Invalid CIDR netblock - bits in host portion of address of #{self.params}" ) end end @@ -513,14 +505,15 @@ def parse_params(required = true) def params return nil unless @ip_network return @ip_network if String === @ip_network - params = @ip_network.to_addr - params += '/' + @ip_network.pfxlen.to_s if - @ip_network.pfxlen != self.default_ipv6_prefix_length + # Render the mechanism's original, unmasked address. + params = IPAddr.new(@ip_address).to_s + params += '/' + @ip_network.prefix.to_s if + @ip_network.prefix != self.default_ipv6_prefix_length return params end def match(server, request, want_result = true) - return @ip_network.contains?(request.ip_address_v6) + return @ip_network.include?(request.ip_address_v6) end end diff --git a/lib/spf/request.rb b/lib/spf/request.rb index e5e2248..07e1f72 100644 --- a/lib/spf/request.rb +++ b/lib/spf/request.rb @@ -1,5 +1,5 @@ # encoding: ASCII-8BIT -require 'ip' +require 'ipaddr' require 'spf/error' @@ -101,9 +101,9 @@ def initialize(options = {}) return unless @ip_address - # Ensure ip_address is an IP object: - unless IP === @ip_address - @ip_address = IP.new(@ip_address) + # Ensure ip_address is an IPAddr object: + unless IPAddr === @ip_address + @ip_address = IPAddr.new(@ip_address) end # Convert IPv4 address to IPv4-mapped IPv6 address: @@ -111,9 +111,9 @@ def initialize(options = {}) if SPF::Util.ipv6_address_is_ipv4_mapped(@ip_address) @ip_address_v6 = @ip_address # Accept as IPv6 address as-is @ip_address = SPF::Util.ipv6_address_to_ipv4(@ip_address) - elsif IP::V4 === @ip_address + elsif @ip_address.ipv4? @ip_address_v6 = SPF::Util.ipv4_address_to_ipv6(@ip_address) - elsif IP::V6 === @ip_address + elsif @ip_address.ipv6? @ip_address_v6 = @ip_address else raise SPF::InvalidOptionValueError.new("Unexpected IP address version"); diff --git a/lib/spf/test/scenario.rb b/lib/spf/test/scenario.rb index 6e7940c..8aa60a2 100644 --- a/lib/spf/test/scenario.rb +++ b/lib/spf/test/scenario.rb @@ -1,6 +1,6 @@ require 'spf/test' -require 'ip' +require 'ipaddr' require 'yaml' #class Resolv::DNS::Resource::IN::SPF < Resolv::DNS::Resource::IN::TXT @@ -56,7 +56,7 @@ def self.new_from_yaml_struct(yaml_struct, options = {}) end end elsif type == 'A' or type == 'AAAA' - address = IP.new(data_struct).to_s + address = IPAddr.new(data_struct).to_s if type == 'A' record = Resolv::DNS::Resource::IN::A.new(address) else diff --git a/lib/spf/util.rb b/lib/spf/util.rb index 2539d8c..df5d3bc 100644 --- a/lib/spf/util.rb +++ b/lib/spf/util.rb @@ -1,5 +1,5 @@ # encoding: ASCII-8BIT -require 'ip' +require 'ipaddr' require 'socket' require 'spf/error' @@ -51,15 +51,15 @@ def self.hostname end def self.ipv4_address_to_ipv6(ipv4_address) - unless IP::V4 === ipv4_address - raise SPF::InvalidOptionValueError.new('IP::V4 address expected') + unless IPAddr === ipv4_address and ipv4_address.ipv4? + raise SPF::InvalidOptionValueError.new('IPv4 address expected') end - return IP.new("::ffff:#{ipv4_address.to_addr}/#{ipv4_address.pfxlen - 32 + 128}") + return IPAddr.new("::ffff:#{ipv4_address.to_s}/#{ipv4_address.prefix - 32 + 128}") end def self.ipv6_address_to_ipv4(ipv6_address) - unless IP::V6 === ipv6_address and ipv6_address.ipv4_mapped? - raise SPF::InvalidOptionValueError, 'IPv4-mapped IP::V6 address expected' + unless IPAddr === ipv6_address and ipv6_address.ipv6? and ipv6_address.ipv4_mapped? + raise SPF::InvalidOptionValueError, 'IPv4-mapped IPv6 address expected' end return ipv6_address.native end @@ -68,25 +68,35 @@ def self.ipv6_address_is_ipv4_mapped(ipv6_address) return ipv6_address.ipv4_mapped? end + # Whether the address portion of a CIDR netblock has bits set outside the + # network prefix (e.g. "1.2.3.4/24" has host bits set, "1.2.3.0/24" does not). + # IPAddr masks such bits away on construction, so detection compares the + # original, unmasked address string against the masked network. + def self.ip_network_has_host_bits?(ip_address_string, ip_network) + return false unless IPAddr === ip_network + return IPAddr.new(ip_address_string.to_s).to_i != ip_network.to_i + rescue IPAddr::Error + return false + end + def self.ip_address_to_string(ip_address) - unless IP::V4 === ip_address or IP::V6 === ip_address - raise SPF::InvalidOptionValueError.new('IP::V4 or IP::V6 address expected') + unless IPAddr === ip_address + raise SPF::InvalidOptionValueError.new('IP address expected') end - return ip_address.to_addr + return ip_address.to_s end def self.ip_address_reverse(ip_address) - unless IP::V4 === ip_address or IP::V6 === ip_address - raise SPF::InvalidOptionValueError.new('IP::V4 or IP::V6 address expected') + unless IPAddr === ip_address + raise SPF::InvalidOptionValueError.new('IP address expected') end # Treat IPv4-mapped IPv6 addresses as IPv4 addresses: ip_address = ipv6_address_to_ipv4(ip_address) if ip_address.ipv4_mapped? - case ip_address - when IP::V4 - octets = ip_address.to_addr.split('.').first(ip_address.pfxlen / 8) + if ip_address.ipv4? + octets = ip_address.to_s.split('.').first(ip_address.prefix / 8) return "#{octets .reverse.join('.')}.in-addr.arpa." - when IP::V6 - nibbles = ip_address.to_hex .split('') .first(ip_address.pfxlen / 4) + elsif ip_address.ipv6? + nibbles = ip_address.to_i.to_s(16).rjust(32, '0').split('').first(ip_address.prefix / 4) return "#{nibbles.reverse.join('.')}.ip6.arpa." end end diff --git a/spec/macrostring_spec.rb b/spec/macrostring_spec.rb index e18dad7..a434a42 100644 --- a/spec/macrostring_spec.rb +++ b/spec/macrostring_spec.rb @@ -4,7 +4,7 @@ before(:each) do @request = SPF::Request.new( identity: 'strong-bad@email.example.com', - ip_address: IP.new('192.0.2.3') + ip_address: IPAddr.new('192.0.2.3') ) @server = SPF::Server.new end @@ -96,7 +96,7 @@ it 'expands the "v" macro letter to the string "ip6" if is ipv6' do request = SPF::Request.new( identity: 'strong-bad@email.example.com', - ip_address: IP.new('2001:DB8::CB01') + ip_address: IPAddr.new('2001:DB8::CB01') ) macro_str = described_class.new( text: '%{v}', @@ -215,7 +215,7 @@ it 'expands "%{ir}.%{v}._spf.%{d2}" correctly with an IPv6 request' do request = SPF::Request.new( identity: 'strong-bad@email.example.com', - ip_address: IP.new('2001:DB8::CB01') + ip_address: IPAddr.new('2001:DB8::CB01') ) macro_str = described_class.new( text: '%{ir}.%{v}._spf.%{d2}', diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 92d4594..e94375b 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -38,8 +38,8 @@ end it 'has correct ip address' do - expect(request.ip_address.is_a?(IP)).to be_truthy - expect(request.ip_address).to eq IP.new('192.168.0.1') + expect(request.ip_address.is_a?(IPAddr)).to be_truthy + expect(request.ip_address).to eq IPAddr.new('192.168.0.1') end it 'has correct helo identity' do @@ -49,7 +49,7 @@ it 'creates sub-request object' do clone = request.new_sub_request(ip_address: '192.168.0.254') expect(clone.identity).to eq 'fred@example.com' - expect(clone.ip_address).to eq IP.new('192.168.0.254') + expect(clone.ip_address).to eq IPAddr.new('192.168.0.254') end end @@ -199,8 +199,8 @@ describe 'IP address validation' do - it 'accepts IP object for ip_address' do - ip_address = IP.new('192.168.0.1') + it 'accepts IPAddr object for ip_address' do + ip_address = IPAddr.new('192.168.0.1') request = SPF::Request.new( identity: 'fred@example.com', ip_address: ip_address @@ -213,7 +213,7 @@ identity: 'fred@example.com', ip_address: '::ffff:192.168.0.1' ) - expect(request.ip_address).to eq IP.new('192.168.0.1') + expect(request.ip_address).to eq IPAddr.new('192.168.0.1') end end diff --git a/spec/util_spec.rb b/spec/util_spec.rb index c13b0b2..de5f523 100644 --- a/spec/util_spec.rb +++ b/spec/util_spec.rb @@ -1,14 +1,14 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -require 'ip' +require 'ipaddr' -ipv4_address = IP.new('192.168.0.1') -ipv6_address_v4mapped = IP.new('::ffff:192.168.0.1') -ipv6_address = IP.new('2001:db8::1') +ipv4_address = IPAddr.new('192.168.0.1') +ipv6_address_v4mapped = IPAddr.new('::ffff:192.168.0.1') +ipv6_address = IPAddr.new('2001:db8::1') describe "SPF::Util.ipv4_address_to_ipv6" do - it "returns IP object" do - expect(SPF::Util.ipv4_address_to_ipv6(ipv4_address).is_a?(IP)).to be_truthy + it "returns IPAddr object" do + expect(SPF::Util.ipv4_address_to_ipv6(ipv4_address).is_a?(IPAddr)).to be_truthy end it "yields correct IPv4-mapped IPv6 address" do @@ -25,8 +25,8 @@ end describe "SPF::Util.ipv6_address_to_ipv4" do - it "returns IP object" do - expect(SPF::Util.ipv6_address_to_ipv4(ipv6_address_v4mapped).is_a?(IP)).to be_truthy + it "returns IPAddr object" do + expect(SPF::Util.ipv6_address_to_ipv4(ipv6_address_v4mapped).is_a?(IPAddr)).to be_truthy end it "yields correct IPv4 address" do diff --git a/spf.gemspec b/spf.gemspec index c20d0b7..dfcaa70 100644 --- a/spf.gemspec +++ b/spf.gemspec @@ -44,14 +44,12 @@ Gem::Specification.new do |s| s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["~> 0.9.1"]) s.add_development_dependency(%q, [">= 10"]) s.add_development_dependency(%q, ["~> 3.10"]) s.add_development_dependency(%q, [">= 6.3.0"]) s.add_development_dependency(%q, [">= 2.4.13"]) s.add_development_dependency(%q) else - s.add_dependency(%q, ["~> 0.9.1"]) s.add_dependency(%q, [">= 10"]) s.add_dependency(%q, ["~> 3.10"]) s.add_dependency(%q, [">= 6.3.0"]) @@ -59,7 +57,6 @@ Gem::Specification.new do |s| s.add_dependency(%q) end else - s.add_dependency(%q, ["~> 0.9.1"]) s.add_dependency(%q, [">= 10"]) s.add_dependency(%q, ["~> 3.10"]) s.add_dependency(%q, [">= 6.3.0"])