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.
@@ -0,0 +1,13 @@
1
+ development:
2
+ host: 127.0.0.1
3
+ port: 1812
4
+ timeout: 10
5
+ secret: secret_key
6
+ dictionary: radius-dictionary
7
+
8
+ production:
9
+ host: 127.0.0.1
10
+ port: 1812
11
+ timeout: 10
12
+ secret: secret_key
13
+ dictionary: radius-dictionary
@@ -0,0 +1,2 @@
1
+ # Initialize Rubius
2
+ Rubius::Rails.init
data/lib/rubius.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubius/string'
2
+
3
+ require 'rubius/exceptions'
4
+ require 'rubius/rails'
5
+ require 'rubius/dictionary'
6
+ require 'rubius/packet'
7
+ require 'rubius/authenticator'
@@ -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,6 @@
1
+ module Rubius
2
+ class Error < RuntimeError; end
3
+ class InvalidDictionaryError < Error; end
4
+ class MissingConfiguration < Error; end
5
+ class MissingEnvironmentConfiguration < Error; end
6
+ 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
@@ -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
@@ -0,0 +1,14 @@
1
+ class String
2
+ def xor(s2)
3
+ if s2.empty?
4
+ self
5
+ else
6
+ a1 = self.unpack("c*")
7
+ a2 = s2.unpack("c*")
8
+
9
+ a2 *= 2 while a2.length < a1.length
10
+
11
+ a1.zip(a2).collect{|c1,c2| c1^c2}.pack("c*")
12
+ end
13
+ end
14
+ 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
+