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 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: