rubius2 0.1.0
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.
- 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
|
+
|