ipaddr_range_set 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -0
- data/README.md +72 -0
- data/Rakefile +10 -0
- data/ipaddr_range_set.gemspec +18 -0
- data/lib/ipaddr_range_set.rb +119 -0
- data/lib/ipaddr_range_set/version.rb +3 -0
- data/test/test_ip_addr_range_set.rb +135 -0
- metadata +55 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
ipaddr_range_set
|
2
|
+
================
|
3
|
+
|
4
|
+
convenience class to create a set of possibly discontiguous IP address range
|
5
|
+
segments, and check if an IP address is in the set.
|
6
|
+
|
7
|
+
Ruby stdlib IPAddr does the heavy-lifting, this is relatively simple code,
|
8
|
+
but which can simplify your own code when used. Having to do this sort
|
9
|
+
of thing is often the sign of a bad design, but many of us have to do it anyway.
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
require 'ipaddr_range_set'
|
14
|
+
|
15
|
+
# Zero or more segment arguments, of a variety
|
16
|
+
# of formats.
|
17
|
+
range = IPAddrRangeSet.new(
|
18
|
+
'220.1.10.3', # an IPv4 as a string
|
19
|
+
'2001:db8::10', # An IPv6 as a string
|
20
|
+
'8.0.0.0/24', # IPv4 as CIDR, IPv6 CIDR too
|
21
|
+
'8.*.*.*', # informal splat notation, only for IPv4
|
22
|
+
'8.8.0.0'..'8.8.2.255', # arbitrary range
|
23
|
+
IPAddr.new(whatever), # arbitrary existing IPAddr object
|
24
|
+
(ip_addr..ip_addr) # range of arbitrary IPAddr objects.
|
25
|
+
)
|
26
|
+
|
27
|
+
When ruby Range's are used, IPAddrRangeSegment makes sure to use `Range#cover?`
|
28
|
+
internally, not `Range#include?` (the latter being disastrous for anything that
|
29
|
+
doesn't have `#to_int`). Triple dot `...` exclusive ranges are supported, if for
|
30
|
+
some reason you want them.
|
31
|
+
|
32
|
+
range.include? '220.1.10.5'
|
33
|
+
range.include? IPAddr.new('220.1.10.5')
|
34
|
+
|
35
|
+
`#include?` is aliased as `#===` so you can easily use it in `case/when`.
|
36
|
+
|
37
|
+
IPAddrRangeSets are immutable, but you can create new ones combining existing
|
38
|
+
ranges:
|
39
|
+
|
40
|
+
new_range = IPAddrRangeSet('8.10.5.1') + IPAddrRangeSet('8.11.6.1')
|
41
|
+
new_range = IPAddrRangeSet('8.10.5.1').add('8.0.0.0/24')
|
42
|
+
|
43
|
+
The internal implementation just steps through all range segments and checks
|
44
|
+
the argument for inclusion, there's no special optimization to detect overlapping
|
45
|
+
ranges and simplify them. If you are doing a high enough volume of segment/arg
|
46
|
+
checks that you need performance, you probably need a custom implementation
|
47
|
+
involving a search tree of some kind anyway.
|
48
|
+
|
49
|
+
As above range 'union' is supported, but range intersection is not. It's
|
50
|
+
a bit tricky to implement well, and I don't have a use case for it.
|
51
|
+
|
52
|
+
Built-in constants are available for local (private, not publically routable)
|
53
|
+
and loopback ranges in both IPv4 IPv6. IPv4Local, IPv4Loopback, IPv6Local,
|
54
|
+
IPv6Loopback. The constant `LocalAddresses` is the union of v4 and v6 local
|
55
|
+
and loopback addresses.
|
56
|
+
|
57
|
+
IPAddrRangeSet::LocalAddress.include? "127.0.0.1" # true
|
58
|
+
IPAddrRangeSet::LocalAddress.include? "10.0.0.1" # true
|
59
|
+
IPAddrRangeSet::LocalAddress.include? "192.168.0.1" # true
|
60
|
+
IPAddrRangeSet::LocalAddress.include? "::1" # true, ipv6 loopback
|
61
|
+
IPAddrRangeSet::LocalAddress.include? "fc00::1" # an ipv6 local
|
62
|
+
|
63
|
+
== Note on ipv6
|
64
|
+
|
65
|
+
It supports ipv6 just because it was so easy to do so with the underlying
|
66
|
+
IPAddr implementation. But I don't have much experience or use for IPv6, there
|
67
|
+
could be oddities hiding in there.
|
68
|
+
|
69
|
+
You can create an IPAddrRangeSet that includes both IPv4 and IPv6 segments, no
|
70
|
+
problem. But an individual `include?` argument will only match a segment of
|
71
|
+
it's own type, no automatic conversion of IPv4-compatible IPv6 addresses
|
72
|
+
is done (should it be? I have no idea, don't really understand ipv6 use cases).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ipaddr_range_set/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ipaddr_range_set"
|
7
|
+
s.version = IpaddrRangeSet::VERSION
|
8
|
+
s.authors = ["Jonathan Rochkind"]
|
9
|
+
s.email = ["jonathan@dnil.net"]
|
10
|
+
s.homepage = "https://github.com/jrochkind/ipaddr_range_set"
|
11
|
+
s.summary = %q{convenience class to create a set of possibly discontiguous IP address range
|
12
|
+
segments, and check if an IP address is in the set.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require "ipaddr_range_set/version"
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
|
5
|
+
class IPAddrRangeSet
|
6
|
+
|
7
|
+
def initialize(*segments_list)
|
8
|
+
|
9
|
+
segments_list.each do |segment|
|
10
|
+
|
11
|
+
if IPAddr === segment
|
12
|
+
segment.freeze
|
13
|
+
segments << segment
|
14
|
+
next
|
15
|
+
end
|
16
|
+
|
17
|
+
if Range === segment
|
18
|
+
first = segment.first
|
19
|
+
last = segment.last
|
20
|
+
|
21
|
+
first = IPAddr.new(first) unless first.kind_of? IPAddr
|
22
|
+
last = IPAddr.new(last) unless last.kind_of? IPAddr
|
23
|
+
|
24
|
+
segments << Range.new(first, last, segment.exclude_end?).freeze
|
25
|
+
next
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
segment = segment.to_str
|
30
|
+
|
31
|
+
# special splat processing? eg '124.*.*.*' ipv4 only.
|
32
|
+
# Convert to ordinary 'a.b.c.d/m' CIDR notation.
|
33
|
+
if segment.include?('*') && segment =~ /(\d{1,3}|\*\.){3}\d{1,3}|\*/
|
34
|
+
octets = segment.split('.')
|
35
|
+
|
36
|
+
if (octets.rindex {|o| o =~ /\d+/}) > octets.rindex("*")
|
37
|
+
raise ArgumentError.new("Invalid splat range, all *s have to come before all concrete octets")
|
38
|
+
end
|
39
|
+
|
40
|
+
splats = 0
|
41
|
+
base = octets.collect do |o|
|
42
|
+
if o == '*'
|
43
|
+
splats += 1
|
44
|
+
'0'
|
45
|
+
else
|
46
|
+
o
|
47
|
+
end
|
48
|
+
end.join(".")
|
49
|
+
|
50
|
+
prefix_size = 32 - (8 * splats)
|
51
|
+
segments << IPAddr.new("#{base}/#{prefix_size}")
|
52
|
+
|
53
|
+
next
|
54
|
+
end
|
55
|
+
|
56
|
+
segments << IPAddr.new(segment)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Does the range set include the argument?
|
61
|
+
# Can pass in IPAddr or string IP addr (that will be used as arg to IPAddr.new)
|
62
|
+
#
|
63
|
+
# Aliased as #=== (for case/when!) and cover?
|
64
|
+
def include?(ip_addr)
|
65
|
+
ip_addr = IPAddr.new(ip_addr) unless ip_addr.kind_of? IPAddr
|
66
|
+
|
67
|
+
segments.each do |segment|
|
68
|
+
# important to use cover? and not include? on Ranges, to avoid
|
69
|
+
# terribly inefficient check. But if segment is an IPAddr, you want include?
|
70
|
+
if segment.respond_to?(:cover)
|
71
|
+
return true if segment.cover? ip_addr
|
72
|
+
else
|
73
|
+
return true if segment.include? ip_addr
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
alias_method :cover?, :include?
|
80
|
+
alias_method :'===', :include?
|
81
|
+
|
82
|
+
# Returns a NEW IPAddrRangeSet composed of union of segments
|
83
|
+
# in receiver and argument. Aliased as `+`
|
84
|
+
#
|
85
|
+
# IPAddrRangeSets are immutable.
|
86
|
+
def union(other_set)
|
87
|
+
all_segments = self.segments + other_set.segments
|
88
|
+
self.class.new *all_segments
|
89
|
+
end
|
90
|
+
alias_method :'+', :union
|
91
|
+
|
92
|
+
# Returns a NEW IPAddrRangeSet composed of union of receiver,
|
93
|
+
# and additional segments(s) given as arguments.
|
94
|
+
# IPAddrRangeSet is immutable.
|
95
|
+
def add(*new_segments)
|
96
|
+
return self + IPAddrRangeSet.new(*new_segments)
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
# Not public API, but used for creating unions of range sets,
|
102
|
+
# etc., is why it's protected instead of private
|
103
|
+
def segments
|
104
|
+
@segments ||= []
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
class IPAddrRangeSet
|
111
|
+
# Constant ranges for local/non-routable/private addresses
|
112
|
+
IPv4Local = IPAddrRangeSet.new("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
113
|
+
IPv4Loopback = IPAddrRangeSet.new("127.0.0.0/8")
|
114
|
+
|
115
|
+
IPv6Local = IPAddrRangeSet.new("fc00::/7")
|
116
|
+
IPv6Loopback = IPAddrRangeSet.new("::1")
|
117
|
+
|
118
|
+
LocalAddresses = IPv4Local + IPv4Local + IPv6Local + IPv6Local
|
119
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
require 'ipaddr_range_set'
|
4
|
+
|
5
|
+
|
6
|
+
class TestIpAddrRange < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def self.test_inclusion(label, argument, should_include, *should_not_include)
|
9
|
+
self.send(:define_method , "test_#{label}" ) do
|
10
|
+
range = IPAddrRangeSet.new(argument)
|
11
|
+
assert range.include?(should_include), "IPAddrRangeSet.new #{argument} should include? #{should_include}"
|
12
|
+
|
13
|
+
should_not_include.each do | should_not|
|
14
|
+
assert !range.include?(should_not), "IPAddrRangeSet.new #{argument} should NOT include? #{should_not}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# label (suitable for part of method name), then
|
20
|
+
# init argument, arg that should be included, 1 to more args that should not be included
|
21
|
+
test_inclusion("single_ipv4_str", "128.220.0.1", "128.220.0.1", "128.220.0.2", "128.220.0.0" )
|
22
|
+
test_inclusion("single_ipv4_obj", IPAddr.new("128.220.0.1"), "128.220.0.1", "128.220.0.2" )
|
23
|
+
test_inclusion("single_ipv4_obj_obj", IPAddr.new("128.220.0.1"), IPAddr.new("128.220.0.1"), IPAddr.new("128.220.0.2") )
|
24
|
+
|
25
|
+
test_inclusion("single_ipv6_str", "2607:f0d0:1002:51::4", "2607:f0d0:1002:51::4", "2607:f0d0:1002:51::5", "2607:f0d0:1002:51::3" )
|
26
|
+
test_inclusion("single_ipv6_obj", IPAddr.new("2607:f0d0:1002:51::4"), "2607:f0d0:1002:51::4", "2607:f0d0:1002:51::5", "2607:f0d0:1002:51::3" )
|
27
|
+
|
28
|
+
test_inclusion("test_ipv4_cidr", "128.220.10.1/24", "128.220.10.100", "128.220.9.255", "128.220.11.1" )
|
29
|
+
|
30
|
+
test_inclusion("test_ipv6_cidr", "2001:db8::/50", "2001:db8::10", "2001:db7::", "2001:db9::" )
|
31
|
+
|
32
|
+
test_inclusion("ipv4_range_str", ("128.220.10.1".."128.220.11.255"), "128.220.11.10", "128.220.9.255", '128.220.12.1')
|
33
|
+
test_inclusion("ipv4_range_obj", (IPAddr.new("128.220.10.1")..IPAddr.new("128.220.11.255")), "128.220.11.10", "128.220.9.255", '128.220.12.1')
|
34
|
+
|
35
|
+
test_inclusion("ipv6_range_str", ("2001:db8::10".."2001:db8::15"), "2001:db8::12", "2001:db8::9", '2001:db8::16')
|
36
|
+
|
37
|
+
|
38
|
+
test_inclusion("ipv4_range_str_exclusive_endpoint", ("128.220.10.1"..."128.220.11.255"), "128.220.10.1", "128.220.9.255", "128.220.11.255")
|
39
|
+
|
40
|
+
|
41
|
+
def test_incompatible_range
|
42
|
+
# one ipv4 one ipv6.
|
43
|
+
|
44
|
+
# Somehow get this for free from ruby Range at the moment
|
45
|
+
assert_raise ArgumentError do
|
46
|
+
IPAddrRangeSet.new( ("128.220.0.1".."2001:db8::10") )
|
47
|
+
end
|
48
|
+
|
49
|
+
assert_raise ArgumentError do
|
50
|
+
IPAddrRangeSet.new( (IPAddr.new("128.220.0.1").."2001:db8::10") )
|
51
|
+
end
|
52
|
+
|
53
|
+
assert_raise ArgumentError do
|
54
|
+
IPAddrRangeSet.new( (IPAddr.new("128.220.0.1")..IPAddr.new("2001:db8::10")) )
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_empty_set
|
59
|
+
range = IPAddrRangeSet.new()
|
60
|
+
|
61
|
+
assert ! range.include?("128.220.0.1")
|
62
|
+
assert ! range.include?(IPAddr.new "128.220.0.1")
|
63
|
+
assert ! range.include?(IPAddr.new "2001:db8::10")
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_splats
|
67
|
+
range = IPAddrRangeSet.new("128.*.*.*")
|
68
|
+
|
69
|
+
assert range.include?("128.4.2.1")
|
70
|
+
|
71
|
+
assert ! range.include?("127.0.0.1")
|
72
|
+
|
73
|
+
assert ! range.include?("129.1.1.1")
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_bad_input
|
77
|
+
assert_raise(ArgumentError) { IPAddrRangeSet.new("foo")}
|
78
|
+
|
79
|
+
assert_raise(ArgumentError) { IPAddrRangeSet.new("124.*")}
|
80
|
+
|
81
|
+
# splats have to be at end
|
82
|
+
assert_raise(ArgumentError) { IPAddrRangeSet.new("124.*.1.1")}
|
83
|
+
|
84
|
+
# Not a valid ipv4, make sure we catch it on ranges
|
85
|
+
assert_raise(ArgumentError) { IPAddrRangeSet.new("124.999.1.1".."125.0.0.1") }
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_multi_arg
|
89
|
+
range = IPAddrRangeSet.new("128.220.1.1", "128.221.*.*", "128.222.0.0/16", ("128.223.1.0".."128.224.255.255"))
|
90
|
+
|
91
|
+
assert ! range.include?("128.220.10.1")
|
92
|
+
|
93
|
+
assert range.include?("128.220.1.1")
|
94
|
+
assert range.include?("128.221.10.1")
|
95
|
+
assert range.include?("128.222.10.1")
|
96
|
+
assert range.include?("128.224.1.1")
|
97
|
+
|
98
|
+
assert ! range.include?("128.225.0.0")
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_union
|
102
|
+
range = IPAddrRangeSet.new("128.220.1.1") + IPAddrRangeSet.new("128.225.1.1")
|
103
|
+
|
104
|
+
assert range.include? "128.220.1.1"
|
105
|
+
assert range.include? "128.225.1.1"
|
106
|
+
|
107
|
+
assert ! range.include?("128.222.1.1")
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_add
|
111
|
+
range = IPAddrRangeSet.new("128.220.1.1")
|
112
|
+
|
113
|
+
new_range = range.add("128.225.1.1", "128.230.1.1")
|
114
|
+
|
115
|
+
assert new_range.include? "128.220.1.1"
|
116
|
+
assert new_range.include? "128.225.1.1"
|
117
|
+
assert new_range.include? "128.230.1.1"
|
118
|
+
|
119
|
+
assert ! new_range.include?("128.226.1.1")
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_local_constants
|
124
|
+
%w{10.3.3.1 172.16.4.1 192.168.2.1 fc00::1}.each do |ip|
|
125
|
+
assert IPAddrRangeSet::LocalAddresses.include?(ip), "IPAddrRangeSet::LocalAddresses should include #{ip}"
|
126
|
+
end
|
127
|
+
|
128
|
+
%w{9.1.1.1 173.1.1.1 193.1.1.1 2607:f0d0:1002:51::4}.each do |ip|
|
129
|
+
assert (! IPAddrRangeSet::LocalAddresses.include?(ip)), "IPAddrRangeSet::LocalAddresses should NOT include #{ip}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ipaddr_range_set
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan Rochkind
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-08 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email:
|
16
|
+
- jonathan@dnil.net
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- Gemfile
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- ipaddr_range_set.gemspec
|
25
|
+
- lib/ipaddr_range_set.rb
|
26
|
+
- lib/ipaddr_range_set/version.rb
|
27
|
+
- test/test_ip_addr_range_set.rb
|
28
|
+
homepage: https://github.com/jrochkind/ipaddr_range_set
|
29
|
+
licenses: []
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 1.8.24
|
49
|
+
signing_key:
|
50
|
+
specification_version: 3
|
51
|
+
summary: convenience class to create a set of possibly discontiguous IP address range
|
52
|
+
segments, and check if an IP address is in the set.
|
53
|
+
test_files:
|
54
|
+
- test/test_ip_addr_range_set.rb
|
55
|
+
has_rdoc:
|