ipaddr_range_set 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|