radiusrb 1.0.0.pre
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.
- 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
|