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