rubius2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+