ruby-radius 1.1

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