radiusrb 1.0.0.pre

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.
@@ -0,0 +1,312 @@
1
+ # This file is part of the RadiusRB library for Ruby.
2
+ # Copyright (C) 2011 Davide Guerri <davide.guerri@gmail.com>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 3
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ module RadiusRB
19
+
20
+ require 'digest/md5'
21
+ require 'ipaddr_extensions'
22
+
23
+ class Packet
24
+
25
+ CODES = { 'Access-Request' => 1, 'Access-Accept' => 2,
26
+ 'Access-Reject' => 3, 'Accounting-Request' => 4,
27
+ 'Accounting-Response' => 5, 'Access-Challenge' => 11,
28
+ 'Status-Server' => 12, 'Status-Client' => 13 }
29
+
30
+
31
+ HDRLEN = 1 + 1 + 2 + 16 # size of packet header
32
+ P_HDR = "CCna16a*" # pack template for header
33
+ P_ATTR = "CCa*" # pack template for attribute
34
+
35
+ attr_accessor :code
36
+ attr_reader :id, :attributes, :authenticator
37
+
38
+ def initialize(dictionary, id, data = nil)
39
+ @dict = dictionary
40
+ @id = id
41
+ unset_all_attributes
42
+ if data
43
+ @packed = data
44
+ self.unpack
45
+ end
46
+ self
47
+ end
48
+
49
+ def increment_id
50
+ @id = (@id + 1) & 0xff
51
+ end
52
+
53
+ def to_a
54
+ @attributes.to_a
55
+ end
56
+
57
+ # Generate an authenticator. It will try to use /dev/urandom if
58
+ # possible, or the system rand call if that's not available.
59
+ def gen_auth_authenticator
60
+ if (File.exist?("/dev/urandom"))
61
+ File.open("/dev/urandom") do |urandom|
62
+ @authenticator = urandom.read(16)
63
+ end
64
+ else
65
+ @authenticator = []
66
+ 8.times do
67
+ @authenticator << rand(65536)
68
+ end
69
+ @authenticator = @authenticator.pack("n8")
70
+ end
71
+ end
72
+
73
+ def gen_acct_authenticator(secret)
74
+ # From RFC2866
75
+ # Request Authenticator
76
+ #
77
+ # In Accounting-Request Packets, the Authenticator value is a 16
78
+ # octet MD5 [5] checksum, called the Request Authenticator.
79
+ #
80
+ # The NAS and RADIUS accounting server share a secret. The Request
81
+ # Authenticator field in Accounting-Request packets contains a one-
82
+ # way MD5 hash calculated over a stream of octets consisting of the
83
+ # Code + Identifier + Length + 16 zero octets + request attributes +
84
+ # shared secret (where + indicates concatenation). The 16 octet MD5
85
+ # hash value is stored in the Authenticator field of the
86
+ # Accounting-Request packet.
87
+ #
88
+ # Note that the Request Authenticator of an Accounting-Request can
89
+ # not be done the same way as the Request Authenticator of a RADIUS
90
+ # Access-Request, because there is no User-Password attribute in an
91
+ # Accounting-Request.
92
+ #
93
+ @authenticator = "\000"*16
94
+ @authenticator = Digest::MD5.digest(pack + secret)
95
+ @packed = nil
96
+ @authenticator
97
+ end
98
+
99
+ def gen_response_authenticator(secret, request_authenticator)
100
+ @authenticator = request_authenticator
101
+ @authenticator = Digest::MD5.digest(pack + secret)
102
+ @packed = nil
103
+ @authenticator
104
+ end
105
+
106
+ def validate_acct_authenticator(secret)
107
+ if @authenticator
108
+ original_authenticator = @authenticator
109
+ if gen_acct_authenticator(secret) == original_authenticator
110
+ true
111
+ else
112
+ @authenticator = original_authenticator
113
+ false
114
+ end
115
+ else
116
+ false
117
+ end
118
+ end
119
+
120
+ def set_attribute(name, value)
121
+ @attributes[name] = Attribute.new(@dict, name, value)
122
+ end
123
+
124
+ def unset_attribute(name)
125
+ @attributes.delete(name)
126
+ end
127
+
128
+ def attribute(name)
129
+ if @attributes[name]
130
+ @attributes[name].value
131
+ end
132
+ end
133
+
134
+ def unset_all_attributes
135
+ @attributes = {}
136
+ end
137
+
138
+ def set_encoded_attribute(name, value, secret)
139
+ @attributes[name] = Attribute.new(@dict, name, encode(value, secret))
140
+ end
141
+
142
+ def decode_attribute(name, secret)
143
+ if @attributes[name]
144
+ decode(@attributes[name].value.to_s, secret)
145
+ end
146
+ end
147
+
148
+ def pack
149
+ attstr = ""
150
+ @attributes.values.each do |attribute|
151
+ attstr += attribute.pack
152
+ end
153
+ @packed = [CODES[@code], @id, attstr.length + HDRLEN, @authenticator, attstr].pack(P_HDR)
154
+ end
155
+
156
+ protected
157
+
158
+ def unpack
159
+ @code, @id, len, @authenticator, attribute_data = @packed.unpack(P_HDR)
160
+ @code = CODES.key(@code)
161
+
162
+ unset_all_attributes
163
+
164
+ while attribute_data.length > 0 do
165
+ length = attribute_data.unpack("xC").first.to_i
166
+ attribute_type, attribute_value = attribute_data.unpack("Cxa#{length-2}")
167
+ attribute_type = attribute_type.to_i
168
+
169
+ attribute = @dict.find_attribute_by_id(attribute_type)
170
+ attribute_value = case attribute.type
171
+ when 'string'
172
+ attribute_value
173
+ when 'integer'
174
+ attribute.has_values? ? attribute.find_values_by_id(attribute_value.unpack("N")[0]).name : attribute_value.unpack("N")[0]
175
+ when 'ipaddr'
176
+ attribute_value.unpack("N")[0].to_ip.to_s
177
+ when 'time'
178
+ attribute_value.unpack("N")[0]
179
+ when 'date'
180
+ attribute_value.unpack("N")[0]
181
+ end
182
+
183
+ set_attribute(attribute.name, attribute_value) if attribute
184
+ attribute_data[0, length] = ""
185
+ end
186
+ end
187
+
188
+ def xor_str(str1, str2)
189
+ i = 0
190
+ newstr = ""
191
+ str1.each_byte do |c1|
192
+ c2 = str2.bytes.to_a[i]
193
+ newstr = newstr << (c1 ^ c2)
194
+ i = i+1
195
+ end
196
+ newstr
197
+ end
198
+
199
+ def encode(value, secret)
200
+ lastround = @authenticator
201
+ encoded_value = ""
202
+ # pad to 16n bytes
203
+ value += "\000" * (15-(15 + value.length) % 16)
204
+ 0.step(value.length-1, 16) do |i|
205
+ lastround = xor_str(value[i, 16], Digest::MD5.digest(secret + lastround) )
206
+ encoded_value += lastround
207
+ end
208
+ encoded_value
209
+ end
210
+
211
+ def decode(value, secret)
212
+ decoded_value = ""
213
+ lastround = @authenticator
214
+ 0.step(value.length-1, 16) do |i|
215
+ decoded_value = xor_str(value[i, 16], Digest::MD5.digest(secret + lastround))
216
+ lastround = value[i, 16]
217
+ end
218
+
219
+ decoded_value.gsub!(/\000+/, "") if decoded_value
220
+ decoded_value[value.length, -1] = "" unless (decoded_value.length <= value.length)
221
+ return decoded_value
222
+ end
223
+
224
+ class Attribute
225
+
226
+ attr_reader :dict, :name, :vendor
227
+ attr_accessor :value
228
+
229
+ def initialize dict, name, value, vendor=nil
230
+ @dict = dict
231
+ # This is the cheapest and easiest way to add VSA's!
232
+ if (name && (chunks = name.split('/')) && (chunks.size == 2))
233
+ @vendor = chunks[0]
234
+ @name = chunks[1]
235
+ else
236
+ @name = name
237
+ end
238
+ @vendor ||= vendor
239
+ @value = value.is_a?(Attribute) ? value.to_s : value
240
+ end
241
+
242
+ def vendor?
243
+ !!@vendor
244
+ end
245
+
246
+ def pack
247
+ attribute = if (vendor? && (@dict.vendors.find_by_name(@vendor)))
248
+ @dict.vendors.find_by_name(@vendor).attributes.find_by_name(@name)
249
+ else
250
+ @dict.find_attribute_by_name(@name)
251
+ end
252
+ raise "Undefined attribute '#{@name}'." if attribute.nil?
253
+
254
+ if vendor?
255
+ pack_vendor_specific_attribute attribute
256
+ else
257
+ pack_attribute attribute
258
+ end
259
+ end
260
+
261
+ def inspect
262
+ @value
263
+ end
264
+
265
+ def to_s
266
+ @value
267
+ end
268
+
269
+ private
270
+
271
+ def pack_vendor_specific_attribute attribute
272
+ inside_attribute = pack_attribute attribute
273
+ vid = attribute.vendor.id.to_i
274
+ header = [ 26, inside_attribute.size + 6 ].pack("CC") # 26: Type = Vendor-Specific, 4: length of Vendor-Id field
275
+ header += [ 0, vid >> 16, vid >> 8, vid ].pack("CCCC") # first byte of Vendor-Id is 0
276
+ header + inside_attribute
277
+ end
278
+
279
+ def pack_attribute attribute
280
+ anum = attribute.id
281
+ val = case attribute.type
282
+ when "string"
283
+ @value
284
+ when "integer"
285
+ raise "Invalid value name '#{@value}'." if attribute.has_values? && attribute.find_values_by_name(@value).nil?
286
+ [attribute.has_values? ? attribute.find_values_by_name(@value).id : @value].pack("N")
287
+ when "ipaddr"
288
+ [@value.to_ip.to_i].pack("N")
289
+ when "ipv6addr"
290
+ ipi = @value.to_ip.to_i
291
+ [ ipi >> 96, ipi >> 64, ipi >> 32, ipi ].pack("NNNN")
292
+ when "date"
293
+ [@value].pack("N")
294
+ when "time"
295
+ [@value].pack("N")
296
+ else
297
+ ""
298
+ end
299
+ begin
300
+ [anum,
301
+ val.length + 2,
302
+ val
303
+ ].pack(P_ATTR)
304
+ rescue
305
+ puts "#{@name} => #{@value}"
306
+ puts [anum, val.length + 2, val].inspect
307
+ end
308
+ end
309
+
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,141 @@
1
+ # This file is part of the RadiusRB library for Ruby.
2
+ # Copyright (C) 2011 Davide Guerri <davide.guerri@gmail.com>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 3
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ module RadiusRB
19
+
20
+ require 'socket'
21
+
22
+ class Request
23
+
24
+ def initialize(server, options = {})
25
+ @dict = options[:dict].nil? ? Dictionary.default : options[:dict]
26
+ @nas_ip = options[:nas_ip] || get_my_ip(@host)
27
+ @nas_identifier = options[:nas_identifier] || @nas_ip
28
+ @reply_timeout = options[:reply_timeout].nil? ? 60 : options[:reply_timeout].to_i
29
+ @retries_number = options[:retries_number].nil? ? 1 : options[:retries_number].to_i
30
+
31
+ @host, @port = server.split(":")
32
+
33
+ @port = Socket.getservbyname("radius", "udp") unless @port
34
+ @port = 1812 unless @port
35
+ @port = @port.to_i # just in case
36
+ @socket = UDPSocket.open
37
+ @socket.connect(@host, @port)
38
+ end
39
+
40
+ def authenticate(name, password, secret, user_attributes = {})
41
+ @packet = Packet.new(@dict, Process.pid & 0xff)
42
+ @packet.gen_auth_authenticator
43
+ @packet.code = 'Access-Request'
44
+ @packet.set_attribute('User-Name', name)
45
+ @packet.set_attribute('NAS-Identifier', @nas_identifier)
46
+ @packet.set_attribute('NAS-IP-Address', @nas_ip)
47
+ @packet.set_encoded_attribute('User-Password', password, secret)
48
+
49
+ user_attributes.each_pair do |name, value|
50
+ @packet.set_attribute(name, value)
51
+ end
52
+
53
+ begin
54
+ send_packet
55
+ @recieved_packet = recv_packet(@reply_timeout)
56
+ rescue Exception => e
57
+ retry if (@retries_number -= 1) > 0
58
+ raise
59
+ end
60
+
61
+ reply = { :code => @recieved_packet.code }
62
+ reply.merge @recieved_packet.attributes
63
+ end
64
+
65
+ def accounting_request(status_type, name, secret, sessionid, user_attributes = {})
66
+
67
+ @packet = Packet.new(@dict, Process.pid & 0xff)
68
+ @packet.code = 'Accounting-Request'
69
+
70
+ @packet.set_attribute('User-Name', name)
71
+ @packet.set_attribute('NAS-Identifier', @nas_identifier)
72
+ @packet.set_attribute('NAS-IP-Address', @nas_ip)
73
+ @packet.set_attribute('Acct-Status-Type', status_type)
74
+ @packet.set_attribute('Acct-Session-Id', sessionid)
75
+ @packet.set_attribute('Acct-Authentic', 'RADIUS')
76
+
77
+ user_attributes.each_pair do |name, value|
78
+ @packet.set_attribute(name, value)
79
+ end
80
+
81
+ @packet.gen_acct_authenticator(secret)
82
+
83
+ begin
84
+ send_packet
85
+ @recieved_packet = recv_packet(@reply_timeout)
86
+ rescue Exception => e
87
+ retry if (@retries_number -= 1) > 0
88
+ raise
89
+ end
90
+
91
+ return true
92
+ end
93
+
94
+ def accounting_start(name, secret, sessionid, options = {})
95
+ accounting_request('Start', name, secret, sessionid, options)
96
+ end
97
+
98
+ def accounting_update(name, secret, sessionid, options = {})
99
+ accounting_request('Interim-Update', name, secret, sessionid, options)
100
+ end
101
+
102
+ def accounting_stop(name, secret, sessionid, options = {})
103
+ accounting_request('Stop', name, secret, sessionid, options)
104
+ end
105
+
106
+ def inspect
107
+ to_s
108
+ end
109
+
110
+ private
111
+
112
+ def send_packet
113
+ data = @packet.pack
114
+ @packet.increment_id
115
+ @socket.send(data, 0)
116
+ end
117
+
118
+ def recv_packet(timeout)
119
+ if select([@socket], nil, nil, timeout.to_i) == nil
120
+ raise "Timed out waiting for response packet from server"
121
+ end
122
+ data = @socket.recvfrom(64)
123
+ Packet.new(@dict, Process.pid & 0xff, data[0])
124
+ end
125
+
126
+ #looks up the source IP address with a route to the specified destination
127
+ def get_my_ip(dest_address)
128
+ orig_reverse_lookup_setting = Socket.do_not_reverse_lookup
129
+ Socket.do_not_reverse_lookup = true
130
+
131
+ UDPSocket.open do |sock|
132
+ sock.connect dest_address, 1
133
+ sock.addr.last
134
+ end
135
+ ensure
136
+ Socket.do_not_reverse_lookup = orig_reverse_lookup_setting
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,77 @@
1
+ # This file is part of the RadiusRB library for Ruby.
2
+ # Copyright (C) 2011 Davide Guerri <davide.guerri@gmail.com>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 3
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ module RadiusRB
19
+
20
+ class VendorCollection < Array
21
+
22
+ def initialize
23
+ @collection = {}
24
+ @revcollection = []
25
+ end
26
+
27
+ def add(id, name)
28
+ @collection[name] ||= Vendor.new(name, id)
29
+ @revcollection[id.to_i] ||= @collection[name]
30
+ self << @collection[name]
31
+ end
32
+
33
+ def find_by_name(name)
34
+ @collection[name]
35
+ end
36
+
37
+ def find_by_id(id)
38
+ @revcollection[id.to_i]
39
+ end
40
+
41
+ end
42
+
43
+ class Vendor
44
+
45
+ include RadiusRB
46
+
47
+ attr_reader :name, :id
48
+
49
+ def initialize(name, id)
50
+ @name = name
51
+ @id = id
52
+ @attributes = AttributesCollection.new self
53
+ end
54
+
55
+ def add_attribute(name, id, type)
56
+ @attributes.add(name, id, type)
57
+ end
58
+
59
+ def find_attribute_by_name(name)
60
+ @attributes.find_by_name(name)
61
+ end
62
+
63
+ def find_attribute_by_id(id)
64
+ @attributes.find_by_id(id.to_i)
65
+ end
66
+
67
+ def has_attributes?
68
+ !@attributes.empty?
69
+ end
70
+
71
+ def attributes
72
+ @attributes
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,27 @@
1
+ # This file is part of the RadiusRB library for Ruby.
2
+ # Copyright (C) 2011 Davide Guerri <davide.guerri@gmail.com>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 3
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ module RadiusRB
19
+ module Version
20
+ MAJOR = 1
21
+ MINOR = 0
22
+ PATCH = 0
23
+ BUILD = 'pre'
24
+
25
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
26
+ end
27
+ end
data/lib/radiusrb.rb ADDED
@@ -0,0 +1,75 @@
1
+ # This file is part of the RadiusRB library for Ruby.
2
+ # Copyright (C) 2011 Davide Guerri <davide.guerri@gmail.com>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 3
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ module RadiusRB
19
+
20
+ # :stopdoc:
21
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
22
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
23
+ # :startdoc:
24
+
25
+ # Returns the library path for the module. If any arguments are given,
26
+ # they will be joined to the end of the library path using
27
+ # <tt>File.join</tt>.
28
+ #
29
+ def self.libpath( *args, &block )
30
+ rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
31
+ if block
32
+ begin
33
+ $LOAD_PATH.unshift LIBPATH
34
+ rv = block.call
35
+ ensure
36
+ $LOAD_PATH.shift
37
+ end
38
+ end
39
+ return rv
40
+ end
41
+
42
+ # Returns the lpath for the module. If any arguments are given,
43
+ # they will be joined to the end of the path using
44
+ # <tt>File.join</tt>.
45
+ #
46
+ def self.path( *args, &block )
47
+ rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
48
+ if block
49
+ begin
50
+ $LOAD_PATH.unshift PATH
51
+ rv = block.call
52
+ ensure
53
+ $LOAD_PATH.shift
54
+ end
55
+ end
56
+ return rv
57
+ end
58
+
59
+ # Utility method used to require all files ending in .rb that lie in the
60
+ # directory below this file that has the same name as the filename passed
61
+ # in. Optionally, a specific _directory_ name can be passed in such that
62
+ # the _filename_ does not have to be equivalent to the directory.
63
+ #
64
+ def self.require_all_libs_relative_to( fname, dir = nil )
65
+ dir ||= ::File.basename(fname, '.*')
66
+ search_me = ::File.expand_path(
67
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
68
+
69
+ Dir.glob(search_me).sort.each {|rb| require rb}
70
+ end
71
+
72
+ end # module RadiusRB
73
+
74
+ RadiusRB.require_all_libs_relative_to(__FILE__)
75
+
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'radiusrb'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestRadiusrb < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end