rubius 0.0.1
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/Gemfile +13 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +53 -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 +6 -0
- data/lib/rubius/authenticator.rb +107 -0
- data/lib/rubius/dictionary.rb +57 -0
- data/lib/rubius/packet.rb +154 -0
- data/lib/rubius/rails.rb +18 -0
- data/lib/rubius/string.rb +14 -0
- data/test/helper.rb +18 -0
- data/test/test_rubius.rb +7 -0
- metadata +120 -0
data/lib/rubius.rb
ADDED
@@ -0,0 +1,107 @@
|
|
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
|
+
@nas_ip = UDPSocket.open {|s| s.connect(@host, 1); s.addr.last }
|
23
|
+
|
24
|
+
@identifier = Process.pid && 0xff
|
25
|
+
end
|
26
|
+
|
27
|
+
def init_from_config(config_file)
|
28
|
+
env = defined?(Rails) ? ::Rails.env : 'development'
|
29
|
+
|
30
|
+
config = YAML.load_file(config_file)
|
31
|
+
@host = config[env]["host"]
|
32
|
+
@port = config[env]["port"] if config[env]["port"]
|
33
|
+
@secret = config[env]["secret"]
|
34
|
+
|
35
|
+
if config[env]["dictionary"]
|
36
|
+
dict = File.join(File.dirname(config_file), config[env]["dictionary"])
|
37
|
+
@dictionary.load(dict) if File.exists?(dict)
|
38
|
+
end
|
39
|
+
|
40
|
+
@nas_ip = config[env]["nas_ip"] if config[env]["nas_ip"]
|
41
|
+
|
42
|
+
setup_connection
|
43
|
+
end
|
44
|
+
|
45
|
+
def authenticate(username, password)
|
46
|
+
init_packet
|
47
|
+
|
48
|
+
@packet.code = Rubius::Packet::ACCESS_REQUEST
|
49
|
+
@packet.secret = @secret
|
50
|
+
rand_authenticator
|
51
|
+
|
52
|
+
@packet.set_attribute('User-Name', username)
|
53
|
+
@packet.set_attribute('NAS-IP-Address', @nas_ip)
|
54
|
+
@packet.set_password(password)
|
55
|
+
|
56
|
+
send_packet
|
57
|
+
recv_packet
|
58
|
+
|
59
|
+
return(@packet.code == Rubius::Packet::ACCESS_ACCEPT)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.authenticate(username, password)
|
63
|
+
Rubius::Authenticator.instance.authenticate(username, password)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def init_packet
|
68
|
+
increment_identifier!
|
69
|
+
@packet = Rubius::Packet.new(@dictionary)
|
70
|
+
@packet.identifier = @identifier
|
71
|
+
end
|
72
|
+
|
73
|
+
def increment_identifier!
|
74
|
+
@identifier = (@identifier + 1) & 0xff
|
75
|
+
end
|
76
|
+
|
77
|
+
def setup_connection
|
78
|
+
@sock = UDPSocket.open
|
79
|
+
@sock.connect(@host, @port)
|
80
|
+
end
|
81
|
+
|
82
|
+
def rand_authenticator
|
83
|
+
if (File.exists?("/dev/urandom"))
|
84
|
+
File.open("/dev/urandom") { |rand| @packet.authenticator = rand.read(16) }
|
85
|
+
else
|
86
|
+
@packet.authenticator = [rand(65536), rand(65536), rand(65536), rand(65536), rand(65536), rand(65536), rand(65536), rand(65536)].pack("n8")
|
87
|
+
end
|
88
|
+
@packet.authenticator
|
89
|
+
end
|
90
|
+
|
91
|
+
def send_packet
|
92
|
+
data = @packet.pack
|
93
|
+
increment_identifier!
|
94
|
+
@sock.send(data, 0)
|
95
|
+
end
|
96
|
+
|
97
|
+
def recv_packet
|
98
|
+
if select([@sock], nil, nil, @timeout) == nil
|
99
|
+
raise "Timed out waiting for response packet from server"
|
100
|
+
end
|
101
|
+
data = @sock.recvfrom(65536)
|
102
|
+
@packet.unpack(data[0])
|
103
|
+
@identifier = @packet.identifier
|
104
|
+
return @packet
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rubius
|
2
|
+
class Dictionary
|
3
|
+
VENDOR = 'VENDOR'
|
4
|
+
ATTRIBUTE = 'ATTRIBUTE'
|
5
|
+
VALUE = 'VALUE'
|
6
|
+
NAME = 'NAME'
|
7
|
+
TYPE = 'TYPE'
|
8
|
+
DEFAULT_VENDOR = 0
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@dictionary = Hash.new
|
12
|
+
@dictionary[DEFAULT_VENDOR] = {NAME => ''}
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(dictionary_file)
|
16
|
+
dict_lines = IO.readlines(dictionary_file)
|
17
|
+
|
18
|
+
vendor_id = DEFAULT_VENDOR
|
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
|
+
vendor_id = tokens[2].to_i
|
27
|
+
vendor_name = tokens[1].strip
|
28
|
+
@dictionary[vendor_id] ||= {NAME => vendor_name}
|
29
|
+
when ATTRIBUTE
|
30
|
+
@dictionary[vendor_id][tokens[2].to_i] = {NAME => tokens[1].strip, TYPE => tokens[3].strip}
|
31
|
+
when VALUE
|
32
|
+
@dictionary[vendor_id][tokens[1]] = {tokens[2].strip => tokens[3].to_i}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def vendor_name(vendor_id)
|
38
|
+
@dictionary[vendor_id][NAME]
|
39
|
+
end
|
40
|
+
|
41
|
+
def attribute(vendor_id, attr_id)
|
42
|
+
@dictionary[vendor_id][attr_id] rescue nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def attribute_name(vendor_id, attr_id)
|
46
|
+
attribute(vendor_id, attr_id)[NAME] rescue nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def attribute_type(vendor_id, attr_id)
|
50
|
+
attribute(vendor_id, attr_id)[TYPE] rescue nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def attribute_id(attr_name, vendor_id=0)
|
54
|
+
@dictionary[vendor_id].reject{|k,v| !v.is_a?(Hash) || v[NAME]!=attr_name}.flatten.first
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,154 @@
|
|
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_id, vendor_attribute_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(Dictionary::DEFAULT_VENDOR, 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(Dictionary::DEFAULT_VENDOR, 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
|
+
rcode = RESPONSES.reject{|k,v| v!=@code}.flatten.first
|
123
|
+
|
124
|
+
return [rcode, @identifier, attr_string.length + HEADER_LENGTH, @authenticator, attr_string].pack(PACK_HEADER)
|
125
|
+
end
|
126
|
+
|
127
|
+
def set_vendor_attribute(vendor_id, attr_id, value)
|
128
|
+
attr_name = @dictionary.attribute_name(vendor_id, attr_id)
|
129
|
+
set_attribute(attr_name, value)
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_attribute(attr_name, value)
|
133
|
+
@attributes[attr_name] = value
|
134
|
+
end
|
135
|
+
|
136
|
+
def set_password(password)
|
137
|
+
lastround = @authenticator
|
138
|
+
pwdout = ""
|
139
|
+
password += "\000" * (15-(15+password.length)%16)
|
140
|
+
0.step(password.length-1, 16) {|i|
|
141
|
+
lastround = password[i, 16].xor(Digest::MD5.digest(@secret + lastround))
|
142
|
+
pwdout += lastround
|
143
|
+
}
|
144
|
+
|
145
|
+
set_attribute("User-Password", pwdout)
|
146
|
+
end
|
147
|
+
|
148
|
+
def response_authenticator
|
149
|
+
attributes = ''
|
150
|
+
hash_data = [5, @identifier, attributes.length+HEADER_LENGTH, @authenticator, attributes, @secret].pack(PACK_HEADER)
|
151
|
+
digest = Digest::MD5.digest(hash_data)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
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)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
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 'rubius'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
data/test/test_rubius.rb
ADDED
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubius
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ralph Rooding
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-19 00:00:00 +02:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: shoulda
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.0.0
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: jeweler
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.5.2
|
46
|
+
type: :development
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rcov
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: *id004
|
60
|
+
description: A simply ruby RADIUS authentication gem
|
61
|
+
email: ralph@izerion.com
|
62
|
+
executables: []
|
63
|
+
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files:
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.rdoc
|
69
|
+
files:
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.rdoc
|
73
|
+
- Rakefile
|
74
|
+
- VERSION
|
75
|
+
- lib/generators/rubius/install_generator.rb
|
76
|
+
- lib/generators/rubius/templates/radius-dictionary
|
77
|
+
- lib/generators/rubius/templates/rubius.yml
|
78
|
+
- lib/generators/rubius/templates/rubius_initializer.rb
|
79
|
+
- lib/rubius.rb
|
80
|
+
- lib/rubius/authenticator.rb
|
81
|
+
- lib/rubius/dictionary.rb
|
82
|
+
- lib/rubius/packet.rb
|
83
|
+
- lib/rubius/rails.rb
|
84
|
+
- lib/rubius/string.rb
|
85
|
+
- test/helper.rb
|
86
|
+
- test/test_rubius.rb
|
87
|
+
has_rdoc: true
|
88
|
+
homepage: http://github.com/rahvin/rubius
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
hash: 4409384527015067188
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
version: "0"
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: "0"
|
111
|
+
requirements: []
|
112
|
+
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.5.2
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: A simply ruby RADIUS authentication gem
|
118
|
+
test_files:
|
119
|
+
- test/helper.rb
|
120
|
+
- test/test_rubius.rb
|