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.
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'rubius/string'
2
+
3
+ require 'rubius/rails'
4
+ require 'rubius/dictionary'
5
+ require 'rubius/packet'
6
+ require 'rubius/authenticator'
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestRubius < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
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