radiusrb 1.0.0.pre

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