ruby-radius 1.1

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,350 @@
1
+ # Radius Dictionary file reader
2
+ # Copyright (C) 2002 Rafael R. Sevilla <dido@imperium.ph>
3
+ # This file is part of the Radius Authentication Module for Ruby
4
+ #
5
+ # The Radius Authentication Module for Ruby is free software; you can
6
+ # redistribute it and/or modify it under the terms of the GNU Lesser
7
+ # General Public License as published by the Free Software
8
+ # Foundation; either version 2.1 of the License, or (at your option)
9
+ # any later version.
10
+ #
11
+ # The Radius Authentication Module is distributed in the hope that it
12
+ # will be useful, but WITHOUT ANY WARRANTY; without even the implied
13
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
+ # See the GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with the GNU C Library; if not, write to the Free
18
+ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
19
+ # 02111-1307 USA.
20
+ #
21
+ # Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
22
+ # Copyright:: Copyright (c) 2002 Rafael R. Sevilla
23
+ # License:: GNU Lesser General Public License
24
+ # $Id: dictionary.rb 2 2006-12-17 06:16:21Z dido $
25
+ #
26
+
27
+ module Radius
28
+ # This is a simple class that can read RADIUS dictionary files and
29
+ # parse them, allowing conversion between dictionary names and
30
+ # numbers. Vendor-specific attributes are supported in a way
31
+ # consistent with the standards.
32
+ #
33
+ # This class is patterned after the Net::Radius::Dictionary Perl
34
+ # module written by Christopher Masto (mailto:chris@netmonger.net)
35
+ # and Luis E. Munoz (mailto:lem@cantv.net)
36
+ class Dictionary
37
+ # Initialize all the instance variables. All variables
38
+ # start out as empty versions of the appropriate type.
39
+ def initialize(dictionary_path = nil)
40
+ @attr = Hash.new(nil)
41
+ @rattr = Array.new
42
+ @val = Array.new
43
+ @rval = Array.new
44
+ @vsattr = Array.new
45
+ @rvsattr = Array.new
46
+ @vsaval = Array.new
47
+ @rvsaval = Array.new
48
+ dictionary_path = File.dirname(__FILE__) + '/../../dictionary' unless dictionary_path
49
+ File.open(dictionary_path, 'r') {|f| read(f)}
50
+ end
51
+
52
+ # Parse a dictionary file from an IO object and learn the
53
+ # name<->number mappings from it. Only the /first/ definition
54
+ # will apply if multiple definitions are seen. This method may be
55
+ # called multiple times with different IO objects, reading from
56
+ # several files.
57
+ #
58
+ # ====Parameters
59
+ #
60
+ # +fp+:: IO object from which to read the data
61
+ #
62
+ # ====Return
63
+ # None. Any strangeness in the file results in a message being
64
+ # printed to stderr.
65
+ def read(fp)
66
+ fp.each_line {
67
+ |line|
68
+ next if line =~ /^\#/ # discard comments
69
+ next if (sl = line.split(/\s+/)) == []
70
+ case sl[0].upcase
71
+ when "ATTRIBUTE"
72
+ @attr[sl[1]] = [sl[2].to_i, sl[3]] if (@attr[sl[1]] == nil)
73
+ @rattr[sl[2].to_i] = [sl[1], sl[3]] if (@rattr[sl[2].to_i] == nil)
74
+ when "VALUE"
75
+ if (@attr[sl[1]] == nil)
76
+ $stderr.print("Warning: value given for unknown attribute #{sl[1]}");
77
+ else
78
+ if (@val[@attr[sl[1]][0]] == nil)
79
+ @val[@attr[sl[1]][0]] = {}
80
+ end
81
+ if (@rval[@attr[sl[1]][0]] == nil)
82
+ @rval[@attr[sl[1]][0]] = []
83
+ end
84
+ if (@val[@attr[sl[1]][0]][sl[2]] == nil)
85
+ @val[@attr[sl[1]][0]][sl[2]] = sl[3].to_i
86
+ end
87
+ if (@rval[@attr[sl[1]][0]][sl[3].to_i] == nil)
88
+ @rval[@attr[sl[1]][0]][sl[3].to_i] = sl[2]
89
+ end
90
+ end
91
+ when "VENDORATTR"
92
+ sl[3] = Kernel::Integer(sl[3]) # this gets hex and octal
93
+ # values correctly
94
+ @vsattr[sl[1].to_i] = {} if (@vsattr[sl[1].to_i] == nil)
95
+ @rvsattr[sl[1].to_i] = {} if (@rvsattr[sl[1].to_i] == nil)
96
+
97
+ if (@vsattr[sl[1].to_i][sl[2]] == nil)
98
+ @vsattr[sl[1].to_i][sl[2]] = sl[3..4]
99
+ end
100
+
101
+ if (@rvsattr[sl[1].to_i][sl[3]] == nil)
102
+ @rvsattr[sl[1].to_i][sl[3]] = [sl[2], sl[4]]
103
+ end
104
+ when "VENDORVALUE"
105
+ sl[4] = Kernel::Integer(sl[4])
106
+ if (@vsattr[sl[1].to_i][sl[2]] == nil)
107
+ $stderr.print "Warning: vendor value for unknown vendor attribute #{sl[1]} found - ignored\n"
108
+ else
109
+ sl[1] = sl[1].to_i
110
+ @vsaval[sl[1]] = {} if @vsaval[sl[1].to_i] == nil
111
+ @rvsaval[sl[1]] = {} if @rvsaval[sl[1].to_i] == nil
112
+ if @vsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]] == nil
113
+ @vsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]] = {}
114
+ end
115
+
116
+ if @rvsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]] == nil
117
+ @rvsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]] = []
118
+ end
119
+
120
+ if @vsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]][sl[3]] == nil
121
+ @vsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]][sl[3]] = sl[4]
122
+ end
123
+
124
+ if @rvsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]][sl[4]] == nil
125
+ @rvsaval[sl[1]][@vsattr[sl[1]][sl[2]][0]][sl[4]] = sl[3]
126
+ end
127
+ end
128
+ else
129
+ $stderr.print "Warning: Weird dictionary line: #{line}\n"
130
+ end
131
+ }
132
+ end
133
+
134
+ # Given an attribute name, return the number corresponding to it,
135
+ # based on the dictionary file(s) that have been read.
136
+ #
137
+ # ====Parameters
138
+ # +attrname+:: Name of the attribute whose number is desired.
139
+ #
140
+ # ====Return Value
141
+ # The number corresponding to the appropriate attribute name
142
+ # given.
143
+ def attr_num(attrname)
144
+ if (@attr[attrname] == nil || @attr[attrname][0] == nil)
145
+ return(nil)
146
+ end
147
+ return(@attr[attrname][0])
148
+ end
149
+
150
+ # Given an attribute name, return the corresponding type for it,
151
+ # based on the dictionary file(s) that have been read.
152
+ #
153
+ # ====Parameters
154
+ # +attrname+:: Name of the attribute whose type is desired.
155
+ #
156
+ # ====Return Value
157
+ # The type string corresponding to the appropriate attribute name
158
+ # given. This is either string, ipaddr, integer, or date.
159
+ def attr_type(attrname)
160
+ if (@attr[attrname] == nil || @attr[attrname][1] == nil)
161
+ return(nil)
162
+ end
163
+ return(@attr[attrname][1])
164
+ end
165
+
166
+ # Given an attribute number, return the name corresponding to it,
167
+ # based on the dictionary file(s) that have been read, the reverse
168
+ # of the attr_num method.
169
+ #
170
+ # ====Parameters
171
+ # +attrname+:: Name of the attribute whose number is desired.
172
+ #
173
+ # ====Return Value
174
+ # The number corresponding to the appropriate attribute name
175
+ # given.
176
+ def attr_name(attrnum)
177
+ if (@rattr[attrnum] == nil || @rattr[attrnum][0] == nil)
178
+ return(nil)
179
+ end
180
+ return(@rattr[attrnum][0])
181
+ end
182
+
183
+ # Given an attribute number, return the type of the attribute
184
+ # corresponding to it, based on the dictionary file(s) that have
185
+ # been read.
186
+ #
187
+ # ====Parameters
188
+ # +attrnum+:: Number of the attribute whose type is desired.
189
+ #
190
+ # ====Return Value
191
+ # The number corresponding to the appropriate attribute name
192
+ # given.
193
+ def attr_numtype(attrnum)
194
+ if (@rattr[attrnum] == nil || @rattr[attrnum][1] == nil)
195
+ return(nil)
196
+ end
197
+ return(@rattr[attrnum][1])
198
+ end
199
+
200
+ # Given an attribute number, return true or false depending on
201
+ # whether or not a value has been given for it.
202
+ #
203
+ # ====Parameters
204
+ # +attrnum+:: Number of the attribute whose definition is known
205
+ #
206
+ # ====Return Value
207
+ # True or false depending on whether some value has been given to
208
+ # the attribute
209
+ def attr_has_val(attrnum)
210
+ return(@val[attrnum] != nil)
211
+ end
212
+
213
+ # Alias for attr_has_val. Don't use this; it's confusing.
214
+ #
215
+ # ====Parameters
216
+ # +attrname+:: Name of the attribute whose number is desired.
217
+ #
218
+ # ====Return Value
219
+ # The number corresponding to the appropriate attribute name
220
+ # given.
221
+ def val_has_name(attrnum)
222
+ return(@rval[attrnum] != nil)
223
+ end
224
+
225
+ # Give the number of the named value for the attribute number
226
+ # supplied.
227
+ #
228
+ # ====Parameters
229
+ # +attrnum+:: the attribute number
230
+ # +valname+:: the name of the value
231
+ # ====Return
232
+ # The number of the named value and attribute.
233
+ def val_num(attrnum, valname)
234
+ return(@val[attrnum][valname])
235
+ end
236
+
237
+ # Returns the name of the numbered value for the attribute value
238
+ # supplied. The reverse of val_num.
239
+ #
240
+ # ====Parameters
241
+ # +attrnum+:: the attribute number
242
+ # +valname+:: the name of the value
243
+ # ====Return
244
+ # The name of the numbered value and attribute.
245
+ def val_name(attrnum, valnum)
246
+ return(@rval[attrnum][valnum])
247
+ end
248
+
249
+ # Obtains the code of a vendor-specific attribute given the
250
+ # Vendor-Id and the name of the vendor-specific attribute (e.g. 9
251
+ # for Cisco and 'cisco-avpair').
252
+ #
253
+ # =====Parameters
254
+ # +vendorid+:: the Vendor-Id
255
+ # +name+:: the name of the vendor-specific attribute to query
256
+ # =====Return Value
257
+ # The code for the vendor-specific attribute
258
+ def vsattr_num(vendorid, name)
259
+ return(@vsattr[vendorid][name][0])
260
+ end
261
+
262
+ # Obtains the type of a vendor-specific attribute given the
263
+ # Vendor-Id and the name of the vendor-specific attribute.
264
+ #
265
+ # =====Parameters
266
+ # +vendorid+:: the Vendor-Id
267
+ # +name+:: the name of the vendor-specific attribute to query
268
+ # =====Return Value
269
+ # The type for the vendor-specific attribute
270
+ def vsattr_type(vendorid, name)
271
+ return(@vsattr[vendorid][name][1])
272
+ end
273
+
274
+ # Obtains the name of a vendor-specific attribute given the
275
+ # Vendor-Id and the code of the vendor-specific attribute. The
276
+ # inverse of vsattr_num.
277
+ #
278
+ # =====Parameters
279
+ # +vendorid+:: the Vendor-Id
280
+ # +code+:: the code of the vendor-specific attribute to query
281
+ # =====Return Value
282
+ # The name of the vendor-specific attribute
283
+ def vsattr_name(vendorid, code)
284
+ return(@rvsattr[vendorid][code][0])
285
+ end
286
+
287
+ # Obtains the type of a vendor-specific attribute given the
288
+ # Vendor-Id and the code of the vendor-specific attribute.
289
+ #
290
+ # =====Parameters
291
+ # +vendorid+:: the Vendor-Id
292
+ # +code+:: the code of the vendor-specific attribute to query
293
+ # =====Return Value
294
+ # The type of the vendor-specific attribute
295
+ def vsattr_numtype(vendorid, code)
296
+ return(@rvsattr[vendorid][code][1]) rescue nil
297
+ end
298
+
299
+ # Determines whether the vendor-specific attibute with the
300
+ # Vendor-Id and code given has a value.
301
+ # =====Parameters
302
+ # +vendorid+:: the Vendor-Id
303
+ # +code+:: the code of the vendor-specific attribute to query
304
+ # =====Return Value
305
+ # True or false on whether or not the vendor-specific attribute
306
+ # has a value
307
+ def vsattr_has_val(vendorid, code)
308
+ return(@vsaval[vendorid][code] != nil)
309
+ end
310
+
311
+ # Alias for vsattr_has_val. Don't use this; it's confusing.
312
+ #
313
+ # ====Parameters
314
+ # +vendorid+:: the Vendor-Id
315
+ # +attrnum+:: Name of the attribute whose number is desired.
316
+ #
317
+ # ====Return Value
318
+ # The number corresponding to the appropriate attribute name
319
+ # given.
320
+ def vsaval_has_name(vendorid, attrnum)
321
+ return(@rvsaval[vendorid][attrnum] != nil)
322
+ end
323
+
324
+ # Give the number of the named value for the vendor-specific
325
+ # attribute number supplied.
326
+ #
327
+ # ====Parameters
328
+ # +vendorid+:: the Vendor-Id
329
+ # +attrnum+:: the attribute number
330
+ # +valname+:: the name of the value
331
+ # ====Return
332
+ # The number of the named value and attribute.
333
+ def vsaval_num(vendorid, attrnum, valname)
334
+ return(@vsaval[vendorid][attrnum][valname])
335
+ end
336
+
337
+ # Returns the name of the numbered value for the vendor-specific
338
+ # attribute value supplied. The reverse of val_num.
339
+ #
340
+ # ====Parameters
341
+ # +vendorid+:: the vendor ID
342
+ # +attrnum+:: the attribute number
343
+ # +valname+:: the name of the value
344
+ # ====Return
345
+ # The name of the numbered value and attribute.
346
+ def vsaval_name(vendorid, attrnum, valnum)
347
+ return(@rvsaval[vendorid][attrnum][valnum])
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,504 @@
1
+ # Radius Authentication Module for Ruby
2
+ # Copyright (C) 2002 Rafael R. Sevilla <dido@imperium.ph>
3
+ # This file is part of the Radius Authentication Module for Ruby
4
+ #
5
+ # The Radius Authentication Module for Ruby is free software; you can
6
+ # redistribute it and/or modify it under the terms of the GNU Lesser
7
+ # General Public License as published by the Free Software
8
+ # Foundation; either version 2.1 of the License, or (at your option)
9
+ # any later version.
10
+ #
11
+ # The Radius Authentication Module is distributed in the hope that it
12
+ # will be useful, but WITHOUT ANY WARRANTY; without even the implied
13
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
+ # See the GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with the GNU C Library; if not, write to the Free
18
+ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
19
+ # 02111-1307 USA.
20
+ #
21
+ # Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
22
+ # Copyright:: Copyright (c) 2002 Rafael R. Sevilla
23
+ # License:: GNU Lesser General Public License
24
+ #
25
+ # $Id: packet.rb 2 2006-12-17 06:16:21Z dido $
26
+ #
27
+
28
+ module Radius
29
+ # RADIUS (RFC 2138) specifies a binary packet format which contains
30
+ # various values and attributes. This class provides an interface
31
+ # to turn RADIUS packets into Ruby data structures and vice-versa.
32
+ #
33
+ # Note that the Radius::Packet module does <em>not</em> provide
34
+ # methods for obtaining or transmitting RADIUS packets to and from
35
+ # the network. A client of this module must provide that for
36
+ # himself or herself.
37
+ #
38
+ # This class is patterned after the Net::Radius::Packet Perl
39
+ # module written by Christopher Masto (mailto:chris@netmonger.net).
40
+ require 'digest/md5'
41
+ require 'radius/dictionary'
42
+
43
+ class Packet
44
+ # The code field is returned as a string. As of this writing, the
45
+ # following codes are recognized:
46
+ #
47
+ # Access-Request Access-Accept
48
+ # Access-Reject Accounting-Request
49
+ # Accounting-Response Access-Challenge
50
+ # Status-Server Status-Client
51
+ attr_reader :code
52
+
53
+ # The code may be set to any of the strings described above in the
54
+ # code attribute reader.
55
+ attr_writer :code
56
+
57
+ # The one-byte Identifier used to match requests and responses is
58
+ # obtained as a character.
59
+ attr_reader :identifier
60
+
61
+ # The Identifer used to match RADIUS requests and responses can
62
+ # also be directly set using this.
63
+ attr_writer :identifier
64
+
65
+ # The 16-byte Authenticator field can be read as a character
66
+ # string with this attribute reader.
67
+ attr_reader :authenticator
68
+ # The authenticator field can be changed with this attribute
69
+ # writer.
70
+ attr_writer :authenticator
71
+
72
+ attr_reader :attributes
73
+
74
+ # To initialize the object, pass a Radius::Dictionary object to it.
75
+ def initialize(dict)
76
+ @dict = dict
77
+ @attributes = Hash.new(nil)
78
+ @vsattributes = Array.new
79
+ end
80
+
81
+ private
82
+ # I'd like to think that these methods should be built in
83
+ # the Socket class
84
+ def inet_aton(hostname)
85
+ if (hostname =~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/)
86
+ return((($1.to_i & 0xff) << 24) + (($2.to_i & 0xff) << 16) +
87
+ (($3.to_i & 0xff) << 8) + (($4.to_i & 0xff)))
88
+ end
89
+ return(0)
90
+ end
91
+
92
+ def inet_ntoa(iaddr)
93
+ return(sprintf("%d.%d.%d.%d", (iaddr >> 24) & 0xff, (iaddr >> 16) & 0xff,
94
+ (iaddr >> 8) & 0xff, (iaddr) & 0xff))
95
+ end
96
+
97
+ public
98
+
99
+ VSA_TYPE = 26 # type given to vendor-specific attributes
100
+ # in RFC 2138
101
+
102
+ # Given a raw RADIUS packet <tt>data</tt>, unpacks its contents so
103
+ # it can be analyzed with other methods, (e.g. +code+, +attr+,
104
+ # etc.). It also clears all present attributes.
105
+ #
106
+ # =====Parameters
107
+ # +data+:: The raw RADIUS packet to decode
108
+ def unpack(data)
109
+ p_hdr = "CCna16a*"
110
+ rcodes = {
111
+ 1 => 'Access-Request',
112
+ 2 => 'Access-Accept',
113
+ 3 => 'Access-Reject',
114
+ 4 => 'Accounting-Request',
115
+ 5 => 'Accounting-Response',
116
+ 11 => 'Access-Challenge',
117
+ 12 => 'Status-Server',
118
+ 13 => 'Status-Client'
119
+ }
120
+
121
+ @code, @identifier, len, @authenticator, attrdat = data.unpack(p_hdr)
122
+ @code = rcodes[@code]
123
+
124
+ unset_all
125
+
126
+ while attrdat.length > 0
127
+ length = attrdat.unpack("xC")[0].to_i
128
+ tval, value = attrdat.unpack("Cxa#{length-2}")
129
+
130
+ tval = tval.to_i
131
+ if tval == VSA_TYPE
132
+ # handle vendor-specific attributes
133
+ vid, vtype, vlength = value.unpack("NCC")
134
+ # XXX - How do we calculate the length of the VSA? It's not
135
+ # defined!
136
+
137
+ # XXX - 3COM seems to do things a bit differently. The 'if'
138
+ # below takes care of that. This is based on the
139
+ # Net::Radius code.
140
+ if vid == 429
141
+ # 3COM packet
142
+ vid, vtype = value.unpack("NN")
143
+ vvalue = value.unpack("xxxxxxxxa#{length - 10}")[0]
144
+ else
145
+ vvalue = value.unpack("xxxxxxa#{vlength - 2}")[0]
146
+ end
147
+ type = @dict.vsattr_numtype(vid, vtype)
148
+ if type != nil
149
+ val = case type
150
+ when 'string' then vvalue
151
+ when 'integer'
152
+ (@dict.vsaval_has_name(vid, vtype)) ?
153
+ @dict.vsaval_name(vid, vtype, vvalue.unpack("N")[0]) :
154
+ vvalue.unpack("N")[0]
155
+ when 'ipaddr' then inet_ntoa(vvalue)
156
+ when 'time' then vvalue.unpack("N")[0]
157
+ when 'date' then vvalue.unpack("N")[0]
158
+ else
159
+ raise "Unknown VSattribute type found: #{vtype}"
160
+ end
161
+ set_vsattr(vid, @dict.vsattr_name(vid, vtype), val)
162
+ end
163
+ else
164
+ type = @dict.attr_numtype(tval)
165
+ unless type.nil?
166
+ val = case type
167
+ when 'string' then value
168
+ when 'integer'
169
+ @dict.val_has_name(tval) ?
170
+ @dict.val_name(tval, value.unpack("N")[0]) :
171
+ value.unpack("N")[0]
172
+ when 'ipaddr' then inet_ntoa(value.unpack("N")[0])
173
+ when 'time' then value.unpack("N")[0]
174
+ when 'date' then value.unpack("N")[0]
175
+ when 'octets' then value
176
+ else raise "Unknown attribute type found: #{type}"
177
+ end
178
+ set_attr(@dict.attr_name(tval), val)
179
+ end
180
+ end
181
+ attrdat[0, length] = ""
182
+ end
183
+ end
184
+
185
+ # The Radius::Packet object contains attributes that can be set
186
+ # and altered with the object's accessor methods, or obtained from
187
+ # the unpack method. This method will return a raw RADIUS
188
+ # packet that should be suitable for sending to a RADIUS client or
189
+ # server over UDP as per RFC 2138.
190
+ #
191
+ # =====Return Value
192
+ # The RADIUS packet corresponding to the object's current internal
193
+ # state.
194
+ def pack
195
+ hdrlen = 1 + 1 + 2 + 16 # size of packet header
196
+ p_hdr = "CCna16a*" # pack template for header
197
+ p_attr = "CCa*" # pack template for attribute
198
+ p_vsa = "CCNCCa*" # pack template for VSA's
199
+ p_vsa_3com = "CCNNa*" # used by 3COM devices
200
+
201
+ codes = {
202
+ 'Access-Request' => 1,
203
+ 'Access-Accept' => 2,
204
+ 'Access-Reject' => 3,
205
+ 'Accounting-Request' => 4,
206
+ 'Accounting-Response' => 5,
207
+ 'Access-Challenge' => 11,
208
+ 'Status-Server' => 12,
209
+ 'Status-Client' => 13
210
+ }
211
+ attstr = ""
212
+ each do |attr, value|
213
+ anum = @dict.attr_num(attr)
214
+ val = case @dict.attr_type(attr)
215
+ when "string" then value
216
+ when "integer"
217
+ [@dict.attr_has_val(anum) ?
218
+ @dict.val_num(anum, value) ? @dict.val_num(anum, value) : value : value].pack("N")
219
+ when "ipaddr" then [inet_aton(value)].pack("N")
220
+ when "date" then [value].pack("N")
221
+ when "time" then [value].pack("N")
222
+ when "octets" then value
223
+ else
224
+ next
225
+ end
226
+ attstr += [@dict.attr_num(attr), val.length + 2, val].pack(p_attr)
227
+ end
228
+
229
+ # Pack vendor-specific attributes
230
+ each_vsa do |vendor, attr, datum|
231
+ code = @dict.vsattr_num(vendor, attr)
232
+ vval = case @dict.vsattr_type(vendor, attr)
233
+ when "string" then datum
234
+ when "integer"
235
+ @dict.vsattr_has_val(vendor.to_i, code) ?
236
+ [@dict.vsaval_num(vendor, code, datum)].pack("N") :
237
+ [datum].pack("N")
238
+ when "ipaddr" then inet_aton(datum)
239
+ when "time" then [datum].pack("N")
240
+ when "date" then [datum].pack("N")
241
+ when "octets" then value
242
+ else
243
+ next
244
+ end
245
+ if vendor == 429
246
+ # For 3COM devices
247
+ attstr += [VSA_TYPE, vval.length + 10, vendor,
248
+ @dict.vsattr_num(vendor, attr), vval].pack(p_vsa_3com)
249
+ else
250
+ attstr += [VSA_TYPE, vval.length + 8, vendor,
251
+ @dict.vsattr_num(vendor, attr), vval.length + 2,
252
+ vval].pack(p_vsa)
253
+ end
254
+ end
255
+ return([codes[@code], @identifier, attstr.length + hdrlen,
256
+ @authenticator, attstr].pack(p_hdr))
257
+ end
258
+
259
+ # This method is provided a block which will pass every
260
+ # attribute-value pair currently available.
261
+ def each
262
+ @attributes.each_pair do |key, value|
263
+ yield(key, value)
264
+ end
265
+ end
266
+
267
+ # The value of the named attribute in the object's internal state
268
+ # can be obtained.
269
+ #
270
+ # ====Parameters
271
+ # +name+:: the name of the attribute to obtain
272
+ #
273
+ # ====Return value:
274
+ # The value of the attribute is returned.
275
+ def attr(name)
276
+ return(@attributes[name])
277
+ end
278
+
279
+ # Changes the value of the named attribute.
280
+ #
281
+ # ====Parameters
282
+ # +name+:: The name of the attribute to set
283
+ # +value+:: The value of the attribute
284
+ def set_attr(name, value)
285
+ @attributes[name] = value
286
+ end
287
+
288
+ # Undefines the current value of the named attribute.
289
+ #
290
+ # ====Parameters
291
+ # +name+:: The name of the attribute to unset
292
+ def unset_attr(name)
293
+ @attributes[name] = nil
294
+ end
295
+
296
+ # Undefines all attributes.
297
+ #
298
+ def unset_all_attr
299
+ each do |key, value|
300
+ unset_attr(key)
301
+ end
302
+ end
303
+
304
+ # This method will pass each vendor-specific attribute available
305
+ # to a passed block. The parameters to the block are the vendor
306
+ # ID, the attribute name, and the attribute value.
307
+ def each_vsa
308
+ @vsattributes.each_index do |vendorid|
309
+ if @vsattributes[vendorid] != nil
310
+ @vsattributes[vendorid].each_pair do |key, value|
311
+ value.each do |val|
312
+ yield(vendorid, key, val)
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ # This method is an iterator that passes each vendor-specific
320
+ # attribute associated with a vendor ID.
321
+ def each_vsaval(vendorid)
322
+ @vsattributes[vendorid].each_pair do |key, value|
323
+ yield(key, value)
324
+ end
325
+ end
326
+
327
+ # Changes the value of the named vendor-specific attribute.
328
+ #
329
+ # ====Parameters
330
+ # +vendorid+:: The vendor ID for the VSA to set
331
+ # +name+:: The name of the attribute to set
332
+ # +value+:: The value of the attribute
333
+ def set_vsattr(vendorid, name, value)
334
+ if @vsattributes[vendorid] == nil
335
+ @vsattributes[vendorid] = Hash.new(nil)
336
+ end
337
+ if @vsattributes[vendorid][name] == nil
338
+ @vsattributes[vendorid][name] = Array.new
339
+ end
340
+ @vsattributes[vendorid][name].push(value)
341
+ end
342
+
343
+ # Undefines the current value of the named vendor-specific
344
+ # attribute.
345
+ #
346
+ # ====Parameters
347
+ # +vendorid+:: The vendor ID for the VSA to set
348
+ # +name+:: The name of the attribute to unset
349
+ def unset_vsattr(vendorid, name)
350
+ return if @vsattributes[vendorid] == nil
351
+ @vsattributes[vendorid][name] = nil
352
+ end
353
+
354
+ # Undefines all vendor-specific attributes.
355
+ #
356
+ def unset_all_vsattr
357
+ each_vsa do |vendor, attr, datum|
358
+ unset_vsattr(vendor, attr)
359
+ end
360
+ end
361
+
362
+ # Undefines all regular and vendor-specific attributes
363
+ def unset_all
364
+ unset_all_attr
365
+ unset_all_vsattr
366
+ end
367
+
368
+ # This method obtains the value of a vendor-specific attribute,
369
+ # given the vendor ID and the name of the vendor-specific
370
+ # attribute.
371
+ # ====Parameters
372
+ # +vendorid+:: the vendor ID
373
+ # +name+:: the name of the attribute to obtain
374
+ #
375
+ # ====Return value:
376
+ # The value of the vendor-specific attribute is returned.
377
+ def vsattr(vendorid, name)
378
+ return(nil) if @vsattributes[vendorid] == nil
379
+ return(@vsattributes[vendorid][name])
380
+ end
381
+
382
+ private
383
+
384
+ # Exclusive-or character by character two strings.
385
+ # returns a new string that is the xor of str1 and str2. The
386
+ # two strings must be the same length.
387
+ def xor_str(str1, str2)
388
+ raise RuntimeError unless str1.size == str2.size
389
+
390
+ memo = ''.encode('US-ASCII')
391
+ str1.bytes.zip(str2.bytes).map {|c1, c2| c1 ^ c2 }.reduce(memo) {|str, i| str << i }
392
+ end
393
+
394
+ public
395
+
396
+ # The RADIUS User-Password attribute is encoded with a shared
397
+ # secret. This method will return the decoded version given the
398
+ # shared secret. This also works when the attribute name is
399
+ # 'Password' for compatibility reasons.
400
+ # ====Parameters
401
+ # +secret+:: The shared secret of the RADIUS system
402
+ # ====Return
403
+ # The cleartext version of the User-Password.
404
+ def password(secret)
405
+ pwdin = attr("User-Password") || attr("Password")
406
+ pwdout = ""
407
+ lastround = @authenticator
408
+
409
+ 0.step(pwdin.length-1, 16) do |i|
410
+ pwdout = xor_str(pwdin[i, 16],
411
+ Digest::MD5.digest(secret + lastround))
412
+ lastround = pwdin[i, 16]
413
+ end
414
+
415
+ pwdout.sub(/\000+$/, "") if pwdout
416
+ pwdout[length.pwdin, -1] = "" unless (pwdout.length <= pwdin.length)
417
+ return(pwdout)
418
+ end
419
+
420
+ def check_password(given, secret)
421
+ given += "\000" * (15 - (15 + given.length) % 16)
422
+
423
+ pwdout = "".force_encoding("ASCII-8BIT")
424
+ lastround = @authenticator
425
+
426
+ 0.step(given.length() -1, 16) do |i|
427
+ lastround = xor_str(given[i, 16], Digest::MD5.digest(secret + lastround))
428
+ pwdout += lastround.force_encoding("ASCII-8BIT")
429
+ end
430
+
431
+ pwdout.sub(/\000+$/, "") if pwdout
432
+ actual_password = @attributes["User-Password"].force_encoding("ASCII-8BIT")
433
+
434
+ pwdout == actual_password
435
+ end
436
+
437
+ # The RADIUS User-Password attribute is encoded with a shared
438
+ # secret. This method will prepare the encoded version of the
439
+ # password. Note that this method <em>always</em> stores the
440
+ # encrypted password in the 'User-Password' attribute. Some
441
+ # (non-RFC 2138-compliant) servers have been reported that insist
442
+ # on using the 'Password' attribute instead.
443
+ #
444
+ # ====Parameters
445
+ # +pwdin++:: The password to encrypt
446
+ # +secret+:: The shared secret of the RADIUS system
447
+ #
448
+ def set_password(pwdin, secret)
449
+ lastround = @authenticator
450
+ pwdout = ""
451
+
452
+ # pad to 16n bytes
453
+ pwdin += "\000" * (15 - (15 + pwdin.length) % 16)
454
+
455
+ 0.step(pwdin.length-1, 16) do |i|
456
+ lastround = xor_str(pwdin[i, 16], Digest::MD5.digest(secret + lastround))
457
+ pwdout += lastround
458
+ end
459
+
460
+ set_attr("User-Password", pwdout)
461
+ return(pwdout)
462
+ end
463
+
464
+ # This method will convert a RADIUS packet into a printable
465
+ # string. Any fields in the packet that might possibly contain
466
+ # non-printable characters are turned into Base64 strings.
467
+ #
468
+ # ====Parameters
469
+ # +secret+:: The shared secret of the RADIUS system. Pass nil if
470
+ # you don't want to see <tt>User-Password</tt> attributes decoded.
471
+ #
472
+ # ====Return
473
+ # The string representation of the RADIUS packet.
474
+ #
475
+ def to_s
476
+ str = "RAD-Code = #{@code}\n"
477
+ str += "RAD-Identifier = #{@identifier}\n"
478
+ str += "RAD-Authenticator = #{[@authenticator].pack('m')}"
479
+ each do |attr, val|
480
+ str += "#{attr} = #{val}\n"
481
+ end
482
+
483
+ each_vsa do |vendorid, vsaname, val|
484
+ str += "Vendor-Id: #{vendorid} -- #{vsaname} = #{val}\n"
485
+ end
486
+ return(str)
487
+ end
488
+
489
+ # Given a (packed) RADIUS packet and a shared secret, returns a
490
+ # new packet with the authenticator field changed in accordance
491
+ # with RADIUS protocol requirements.
492
+ #
493
+ # ====Parameters
494
+ # +packed_packet+:: The packed packet to compute a new Authenticator field for.
495
+ # +secret+:: The shared secret of the RADIUS system.
496
+ # ====Return value
497
+ # a new packed packet with the authenticator field recomputed.
498
+ def Packet.auth_resp(packed_packet, secret)
499
+ new = String.new(packed_packet)
500
+ new[4, 16] = Digest::MD5.digest(packed_packet + secret)
501
+ return(new)
502
+ end
503
+ end
504
+ end