ruby-radius 1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +37 -0
- data/dictionary +289 -0
- data/examples/radiusclient.rb +51 -0
- data/examples/rclient.rb +24 -0
- data/lib/radius/auth.rb +131 -0
- data/lib/radius/dictionary.rb +350 -0
- data/lib/radius/packet.rb +504 -0
- data/test/test.pl +45 -0
- data/test/test.rb +34 -0
- metadata +53 -0
@@ -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
|