rubius2 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +12 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +66 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/generators/rubius/install_generator.rb +11 -0
- data/lib/generators/rubius/templates/radius-dictionary +892 -0
- data/lib/generators/rubius/templates/rubius.yml +13 -0
- data/lib/generators/rubius/templates/rubius_initializer.rb +2 -0
- data/lib/rubius.rb +7 -0
- data/lib/rubius/authenticator.rb +112 -0
- data/lib/rubius/dictionary.rb +86 -0
- data/lib/rubius/exceptions.rb +6 -0
- data/lib/rubius/packet.rb +156 -0
- data/lib/rubius/rails.rb +18 -0
- data/lib/rubius/string.rb +14 -0
- data/rubius.gemspec +87 -0
- data/test/helper.rb +98 -0
- data/test/test_authenticator.rb +84 -0
- data/test/test_dictionary.rb +68 -0
- data/test/test_rails.rb +62 -0
- data/test/test_string.rb +40 -0
- metadata +171 -0
data/lib/rubius.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module Rubius
|
2
|
+
require 'singleton'
|
3
|
+
require 'socket'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
class Authenticator
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@dictionary = Rubius::Dictionary.new
|
11
|
+
@packet = nil
|
12
|
+
@secret = nil
|
13
|
+
|
14
|
+
@host = nil
|
15
|
+
@port ||= Socket.getservbyname("radius", "udp")
|
16
|
+
@port ||= 1812
|
17
|
+
|
18
|
+
@timeout = 10
|
19
|
+
|
20
|
+
@sock = nil
|
21
|
+
|
22
|
+
@identifier = Process.pid & 0xff
|
23
|
+
end
|
24
|
+
|
25
|
+
def init_from_config(config_file, env=nil)
|
26
|
+
if env.nil?
|
27
|
+
env = defined?(::Rails) ? ::Rails.env : 'development'
|
28
|
+
end
|
29
|
+
|
30
|
+
config = YAML.load_file(config_file)
|
31
|
+
raise Rubius::MissingEnvironmentConfiguration unless config.has_key?(env)
|
32
|
+
|
33
|
+
@host = config[env]["host"]
|
34
|
+
@port = config[env]["port"] if config[env]["port"]
|
35
|
+
@secret = config[env]["secret"]
|
36
|
+
|
37
|
+
if config[env]["dictionary"]
|
38
|
+
dict = File.join(File.dirname(config_file), config[env]["dictionary"])
|
39
|
+
@dictionary.load(dict) if File.exists?(dict)
|
40
|
+
end
|
41
|
+
|
42
|
+
@nas_ip = config[env]["nas_ip"] if config[env]["nas_ip"]
|
43
|
+
@nas_ip ||= UDPSocket.open {|s| s.connect(@host, 1); s.addr.last }
|
44
|
+
|
45
|
+
setup_connection
|
46
|
+
rescue Errno::ENOENT
|
47
|
+
raise Rubius::MissingConfiguration
|
48
|
+
end
|
49
|
+
|
50
|
+
def authenticate(username, password)
|
51
|
+
init_packet
|
52
|
+
|
53
|
+
@packet.code = Rubius::Packet::ACCESS_REQUEST
|
54
|
+
@packet.secret = @secret
|
55
|
+
rand_authenticator
|
56
|
+
|
57
|
+
@packet.set_attribute('User-Name', username)
|
58
|
+
@packet.set_attribute('NAS-IP-Address', @nas_ip)
|
59
|
+
@packet.set_password(password)
|
60
|
+
|
61
|
+
send_packet
|
62
|
+
recv_packet
|
63
|
+
|
64
|
+
return(@packet.code == Rubius::Packet::ACCESS_ACCEPT)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.authenticate(username, password)
|
68
|
+
Rubius::Authenticator.instance.authenticate(username, password)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def init_packet
|
73
|
+
increment_identifier!
|
74
|
+
@packet = Rubius::Packet.new(@dictionary)
|
75
|
+
@packet.identifier = @identifier
|
76
|
+
end
|
77
|
+
|
78
|
+
def increment_identifier!
|
79
|
+
@identifier = (@identifier + 1) & 0xff
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_connection
|
83
|
+
@sock = UDPSocket.open
|
84
|
+
@sock.connect(@host, @port)
|
85
|
+
end
|
86
|
+
|
87
|
+
def rand_authenticator
|
88
|
+
if (File.exists?("/dev/urandom"))
|
89
|
+
File.open("/dev/urandom") { |rand| @packet.authenticator = rand.read(16) }
|
90
|
+
else
|
91
|
+
@packet.authenticator = [rand(65536), rand(65536), rand(65536), rand(65536), rand(65536), rand(65536), rand(65536), rand(65536)].pack("n8")
|
92
|
+
end
|
93
|
+
@packet.authenticator
|
94
|
+
end
|
95
|
+
|
96
|
+
def send_packet
|
97
|
+
data = @packet.pack
|
98
|
+
increment_identifier!
|
99
|
+
@sock.send(data, 0)
|
100
|
+
end
|
101
|
+
|
102
|
+
def recv_packet
|
103
|
+
if select([@sock], nil, nil, @timeout) == nil
|
104
|
+
raise "Timed out waiting for response packet from server"
|
105
|
+
end
|
106
|
+
data = @sock.recvfrom(65536)
|
107
|
+
@packet.unpack(data[0])
|
108
|
+
@identifier = @packet.identifier
|
109
|
+
return @packet
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Rubius
|
2
|
+
class Dictionary
|
3
|
+
VENDOR = 'VENDOR'
|
4
|
+
ATTRIBUTE = 'ATTRIBUTE'
|
5
|
+
VALUE = 'VALUE'
|
6
|
+
DEFAULT_VENDOR = 0
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@dictionary = Hash.new
|
10
|
+
@dictionary[DEFAULT_VENDOR] = {:name => ''}
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(dictionary_file)
|
14
|
+
dict_lines = IO.readlines(dictionary_file)
|
15
|
+
|
16
|
+
vendor_id = DEFAULT_VENDOR
|
17
|
+
skip_until_next_vendor = false
|
18
|
+
|
19
|
+
dict_lines.each do |line|
|
20
|
+
next if line =~ /^\#/
|
21
|
+
next if (tokens = line.split(/\s+/)).empty?
|
22
|
+
|
23
|
+
entry_type = tokens[0].upcase
|
24
|
+
case entry_type
|
25
|
+
when VENDOR
|
26
|
+
skip_until_next_vendor = false
|
27
|
+
|
28
|
+
# If the vendor_id string is nil or empty, we should skip this entire block
|
29
|
+
# until we find another VENDOR definition, also ignore all VALUEs and ATTRIBUTEs
|
30
|
+
# until the next VENDOR because otherwise, they will be included in the wrong VENDOR
|
31
|
+
vendor_id_str = tokens[2]
|
32
|
+
if vendor_id_str.nil? || vendor_id_str.empty?
|
33
|
+
skip_until_next_vendor = true
|
34
|
+
next
|
35
|
+
end
|
36
|
+
|
37
|
+
# VENDOR id should be higher than 0, skip everything if it isn't
|
38
|
+
vendor_id = vendor_id_str.to_i
|
39
|
+
if vendor_id <= 0
|
40
|
+
skip_until_next_vendor = true
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
vendor_name = tokens[1].strip
|
45
|
+
@dictionary[vendor_id] ||= {:name => vendor_name}
|
46
|
+
when ATTRIBUTE
|
47
|
+
next if skip_until_next_vendor
|
48
|
+
next if tokens[1].nil? || tokens[2].to_i <= 0 || tokens[3].nil?
|
49
|
+
@dictionary[vendor_id][tokens[2].to_i] = {:name => tokens[1].strip, :type => tokens[3].strip}
|
50
|
+
when VALUE
|
51
|
+
next if skip_until_next_vendor
|
52
|
+
@dictionary[vendor_id][tokens[1]] = {tokens[2].strip => tokens[3].to_i}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue Errno::ENOENT
|
56
|
+
raise Rubius::InvalidDictionaryError
|
57
|
+
end
|
58
|
+
|
59
|
+
def vendors
|
60
|
+
@dictionary.collect{|k,v| v[:name]}.reject{|n| n.empty?}
|
61
|
+
end
|
62
|
+
|
63
|
+
def vendor_name(vendor_id = DEFAULT_VENDOR)
|
64
|
+
@dictionary[vendor_id][:name]
|
65
|
+
end
|
66
|
+
|
67
|
+
def attribute_name(attr_id, vendor_id = DEFAULT_VENDOR)
|
68
|
+
attribute(attr_id, vendor_id)[:name] rescue nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def attribute_type(attr_id, vendor_id = DEFAULT_VENDOR)
|
72
|
+
attribute(attr_id, vendor_id)[:type] rescue nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def attribute_id(attr_name, vendor_id = DEFAULT_VENDOR)
|
76
|
+
vendor_object = @dictionary[vendor_id].reject{|k,v| !v.is_a?(Hash) || v[:name]!=attr_name}
|
77
|
+
vendor_object = vendor_object.to_a if RUBY_VERSION < "1.9.2"
|
78
|
+
vendor_object.flatten.first
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def attribute(attr_id, vendor_id)
|
83
|
+
@dictionary[vendor_id][attr_id] rescue nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Rubius
|
2
|
+
require 'ipaddr'
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
class Packet
|
6
|
+
PACK_HEADER = 'CCna16a*'
|
7
|
+
HEADER_LENGTH = 1 + 1 + 2 + 16
|
8
|
+
VSA_TYPE = 26
|
9
|
+
ACCESS_REQUEST = 'Access-Request'
|
10
|
+
ACCESS_ACCEPT = 'Access-Accept'
|
11
|
+
ACCESS_REJECT = 'Access-Reject'
|
12
|
+
ACCOUNTING_REQUEST = 'Accounting-Request'
|
13
|
+
ACCOUNTING_RESPONSE = 'Accounting-Response'
|
14
|
+
ACCESS_CHALLENGE = 'Access-Challenge'
|
15
|
+
STATUS_SERVER = 'Status-Server'
|
16
|
+
STATUS_CLIENT = 'Status-Client'
|
17
|
+
RESPONSES = { 1 => ACCESS_REQUEST,
|
18
|
+
2 => ACCESS_ACCEPT,
|
19
|
+
3 => ACCESS_REJECT,
|
20
|
+
4 => ACCOUNTING_REQUEST,
|
21
|
+
5 => ACCOUNTING_RESPONSE,
|
22
|
+
11 => ACCESS_CHALLENGE,
|
23
|
+
12 => STATUS_SERVER,
|
24
|
+
13 => STATUS_CLIENT}
|
25
|
+
|
26
|
+
attr_accessor :identifier
|
27
|
+
attr_accessor :secret
|
28
|
+
attr_accessor :code
|
29
|
+
attr_accessor :authenticator
|
30
|
+
|
31
|
+
def initialize(dictionary)
|
32
|
+
@dictionary = dictionary
|
33
|
+
@attributes = Hash.new
|
34
|
+
@secret = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def unpack_attribute(data, type)
|
38
|
+
val = case type
|
39
|
+
when 'string'
|
40
|
+
data
|
41
|
+
when 'integer'
|
42
|
+
data.unpack("N")[0]
|
43
|
+
when 'ipaddr'
|
44
|
+
IPAddr.new(data, Socket::AF_INET).to_s
|
45
|
+
when 'time'
|
46
|
+
data.unpack("N")[0]
|
47
|
+
when 'date'
|
48
|
+
data.unpack("N")[0]
|
49
|
+
else
|
50
|
+
raise "Unknown type found: #{type}"
|
51
|
+
end
|
52
|
+
|
53
|
+
val
|
54
|
+
end
|
55
|
+
private :unpack_attribute
|
56
|
+
|
57
|
+
def pack_attribute(data, type)
|
58
|
+
val = case type
|
59
|
+
when 'string'
|
60
|
+
data
|
61
|
+
when 'integer'
|
62
|
+
[data].pack("N")
|
63
|
+
when 'ipaddr'
|
64
|
+
[IPAddr.new(data).to_i].pack("N")
|
65
|
+
when 'date'
|
66
|
+
[data].pack("N")
|
67
|
+
when 'time'
|
68
|
+
[data].pack("N")
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
val
|
74
|
+
end
|
75
|
+
|
76
|
+
def unpack(data)
|
77
|
+
@code, @identifier, @length, @authenticator, attribute_data = data.unpack(PACK_HEADER)
|
78
|
+
@code = RESPONSES[@code]
|
79
|
+
@attributes = Hash.new
|
80
|
+
|
81
|
+
while(attribute_data.length > 0)
|
82
|
+
# Read the length of the packet data
|
83
|
+
length = attribute_data.unpack("xC")[0].to_i
|
84
|
+
|
85
|
+
# read the type header to determine if this is a VSA
|
86
|
+
type_id, value = attribute_data.unpack("Cxa#{length-2}")
|
87
|
+
type_id = type_id.to_i
|
88
|
+
|
89
|
+
if(type_id == VSA_TYPE)
|
90
|
+
# Handle VSA's
|
91
|
+
vendor_id, vendor_attribute_id, vendor_attribute_length = value.unpack("NCC")
|
92
|
+
vendor_attribute_value = value.unpack("xxxxxxa#{vendor_attribute_length-2}")[0]
|
93
|
+
|
94
|
+
# look up the type of data so we know how to unpack it
|
95
|
+
type = @dictionary.attribute_type(vendor_attribute_id, vendor_id)
|
96
|
+
raise "VSA not found in dictionary (#{vendor_id}/#{vendor_attribute_id})" if type.nil?
|
97
|
+
|
98
|
+
val = unpack_attribute(vendor_attribute_value, type)
|
99
|
+
set_vendor_attribute(vendor_id, vendor_attribute_id, val)
|
100
|
+
else
|
101
|
+
type = @dictionary.attribute_type(type_id)
|
102
|
+
raise "Attribute not found in dictionary (#{Dictionary::DEFAULT_VENDOR}/#{type_id})" if type.nil?
|
103
|
+
|
104
|
+
val = unpack_attribute(value, type)
|
105
|
+
set_vendor_attribute(Dictionary::DEFAULT_VENDOR, type_id, val)
|
106
|
+
end
|
107
|
+
attribute_data[0, length] = ''
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def pack
|
112
|
+
attr_string = ''
|
113
|
+
|
114
|
+
@attributes.each_pair {|key, value|
|
115
|
+
attr_num = @dictionary.attribute_id(key)
|
116
|
+
type = @dictionary.attribute_type(attr_num)
|
117
|
+
val = pack_attribute(value, type)
|
118
|
+
next if val.nil?
|
119
|
+
attr_string += [attr_num, val.length + 2, val].pack("CCa*")
|
120
|
+
}
|
121
|
+
|
122
|
+
rejected_responses = RESPONSES.reject{|k,v| v!=@code}
|
123
|
+
rejected_responses = rejected_responses.to_a if RUBY_VERSION < "1.9.2"
|
124
|
+
rcode = rejected_responses.flatten.first
|
125
|
+
|
126
|
+
return [rcode, @identifier, attr_string.length + HEADER_LENGTH, @authenticator, attr_string].pack(PACK_HEADER)
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_vendor_attribute(vendor_id, attr_id, value)
|
130
|
+
attr_name = @dictionary.attribute_name(attr_id, vendor_id)
|
131
|
+
set_attribute(attr_name, value)
|
132
|
+
end
|
133
|
+
|
134
|
+
def set_attribute(attr_name, value)
|
135
|
+
@attributes[attr_name] = value
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_password(password)
|
139
|
+
lastround = @authenticator
|
140
|
+
pwdout = ""
|
141
|
+
password += "\000" * (15-(15+password.length)%16)
|
142
|
+
0.step(password.length-1, 16) {|i|
|
143
|
+
lastround = password[i, 16].xor(Digest::MD5.digest(@secret + lastround))
|
144
|
+
pwdout += lastround
|
145
|
+
}
|
146
|
+
|
147
|
+
set_attribute("User-Password", pwdout)
|
148
|
+
end
|
149
|
+
|
150
|
+
def response_authenticator
|
151
|
+
attributes = ''
|
152
|
+
hash_data = [5, @identifier, attributes.length+HEADER_LENGTH, @authenticator, attributes, @secret].pack(PACK_HEADER)
|
153
|
+
digest = Digest::MD5.digest(hash_data)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/rubius/rails.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rubius
|
2
|
+
class Rails
|
3
|
+
def self.init(root = nil, env = nil)
|
4
|
+
base_dir = root
|
5
|
+
if root.nil?
|
6
|
+
root = defined?(::Rails) ? ::Rails.root : FileUtils.pwd
|
7
|
+
base_dir = File.expand_path(File.join(root, 'config'))
|
8
|
+
end
|
9
|
+
|
10
|
+
if env.nil?
|
11
|
+
env = defined?(::Rails) ? ::Rails.env : 'development'
|
12
|
+
end
|
13
|
+
|
14
|
+
config_file = File.join(base_dir, 'rubius.yml')
|
15
|
+
Rubius::Authenticator.instance.init_from_config(config_file, env)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/rubius.gemspec
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{rubius2}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Ralph Rooding"]
|
12
|
+
s.date = %q{2015-07-22}
|
13
|
+
s.description = %q{Rubius provides a simple interface to RADIUS authentication}
|
14
|
+
s.email = %q{ralph@izerion.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".autotest",
|
21
|
+
"Gemfile",
|
22
|
+
"LICENSE.txt",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/generators/rubius/install_generator.rb",
|
27
|
+
"lib/generators/rubius/templates/radius-dictionary",
|
28
|
+
"lib/generators/rubius/templates/rubius.yml",
|
29
|
+
"lib/generators/rubius/templates/rubius_initializer.rb",
|
30
|
+
"lib/rubius.rb",
|
31
|
+
"lib/rubius/authenticator.rb",
|
32
|
+
"lib/rubius/dictionary.rb",
|
33
|
+
"lib/rubius/exceptions.rb",
|
34
|
+
"lib/rubius/packet.rb",
|
35
|
+
"lib/rubius/rails.rb",
|
36
|
+
"lib/rubius/string.rb",
|
37
|
+
"rubius.gemspec",
|
38
|
+
"test/helper.rb",
|
39
|
+
"test/test_authenticator.rb",
|
40
|
+
"test/test_dictionary.rb",
|
41
|
+
"test/test_rails.rb",
|
42
|
+
"test/test_string.rb"
|
43
|
+
]
|
44
|
+
s.homepage = %q{http://github.com/bytemine/rubius}
|
45
|
+
s.licenses = ["MIT"]
|
46
|
+
s.require_paths = ["lib"]
|
47
|
+
s.rubygems_version = %q{1.5.2}
|
48
|
+
s.summary = %q{A simple ruby RADIUS authentication gem}
|
49
|
+
s.test_files = [
|
50
|
+
"test/helper.rb",
|
51
|
+
"test/test_authenticator.rb",
|
52
|
+
"test/test_dictionary.rb",
|
53
|
+
"test/test_rails.rb",
|
54
|
+
"test/test_string.rb"
|
55
|
+
]
|
56
|
+
|
57
|
+
if s.respond_to? :specification_version then
|
58
|
+
s.specification_version = 3
|
59
|
+
|
60
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
61
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
62
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
63
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
64
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
65
|
+
s.add_development_dependency(%q<simplecov>, [">= 0.4.0"])
|
66
|
+
s.add_development_dependency(%q<autotest-standalone>, ["~> 4.5.5"])
|
67
|
+
s.add_development_dependency(%q<mocha>, ["~> 0.9.12"])
|
68
|
+
else
|
69
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
70
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
71
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
72
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
73
|
+
s.add_dependency(%q<simplecov>, [">= 0.4.0"])
|
74
|
+
s.add_dependency(%q<autotest-standalone>, ["~> 4.5.5"])
|
75
|
+
s.add_dependency(%q<mocha>, ["~> 0.9.12"])
|
76
|
+
end
|
77
|
+
else
|
78
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
79
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
80
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
81
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
82
|
+
s.add_dependency(%q<simplecov>, [">= 0.4.0"])
|
83
|
+
s.add_dependency(%q<autotest-standalone>, ["~> 4.5.5"])
|
84
|
+
s.add_dependency(%q<mocha>, ["~> 0.9.12"])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|