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 ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ipaddr_range_set.gemspec
4
+ gemspec
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,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ end
8
+
9
+ desc "Run tests"
10
+ task :default => :test
@@ -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,3 @@
1
+ module IpaddrRangeSet
2
+ VERSION = "0.9.0"
3
+ 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: