ruby-sasl 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +22 -0
- data/lib/sasl.rb +4 -0
- data/lib/sasl/anonymous.rb +14 -0
- data/lib/sasl/base.rb +144 -0
- data/lib/sasl/base64.rb +32 -0
- data/lib/sasl/digest_md5.rb +171 -0
- data/lib/sasl/plain.rb +14 -0
- data/spec/anonymous_spec.rb +19 -0
- data/spec/digest_md5_spec.rb +107 -0
- data/spec/mechanism_spec.rb +56 -0
- data/spec/plain_spec.rb +39 -0
- metadata +66 -0
data/README.markdown
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Simple Authentication and Security Layer (RFC 4422) for Ruby
|
2
|
+
============================================================
|
3
|
+
|
4
|
+
Goal
|
5
|
+
----
|
6
|
+
|
7
|
+
Have a reusable library for client implementations that need to do
|
8
|
+
authentication over SASL, mainly targeted at Jabber/XMPP libraries.
|
9
|
+
|
10
|
+
All class carry just state, are thread-agnostic and must also work in
|
11
|
+
asynchronous environments.
|
12
|
+
|
13
|
+
Usage
|
14
|
+
-----
|
15
|
+
|
16
|
+
Derive from **SASL::Preferences** and overwrite the methods. Then,
|
17
|
+
create a mechanism instance:
|
18
|
+
# mechanisms => ['DIGEST-MD5', 'PLAIN']
|
19
|
+
sasl = SASL.new(mechanisms, my_preferences)
|
20
|
+
content_to_send = sasl.start
|
21
|
+
# [...]
|
22
|
+
content_to_send = sasl.challenge(received_content)
|
data/lib/sasl.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module SASL
|
2
|
+
##
|
3
|
+
# SASL ANONYMOUS where you only send a username that may not get
|
4
|
+
# evaluated by the server.
|
5
|
+
#
|
6
|
+
# RFC 4505:
|
7
|
+
# http://tools.ietf.org/html/rfc4505
|
8
|
+
class Anonymous < Mechanism
|
9
|
+
def start
|
10
|
+
@state = nil
|
11
|
+
['auth', preferences.username.to_s]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/sasl/base.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
##
|
2
|
+
# RFC 4422:
|
3
|
+
# http://tools.ietf.org/html/rfc4422
|
4
|
+
module SASL
|
5
|
+
##
|
6
|
+
# You must derive from class Preferences and overwrite methods you
|
7
|
+
# want to implement.
|
8
|
+
class Preferences
|
9
|
+
##
|
10
|
+
# Authorization identitiy ('username@domain' in XMPP)
|
11
|
+
def authzid
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Realm ('domain' in XMPP)
|
17
|
+
def realm
|
18
|
+
raise AbstractMethod
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
##
|
23
|
+
# digest-uri: serv-type/serv-name | serv-type/host/serv-name
|
24
|
+
# ('xmpp/domain' in XMPP)
|
25
|
+
def digest_uri
|
26
|
+
raise AbstractMethod
|
27
|
+
end
|
28
|
+
|
29
|
+
def username
|
30
|
+
raise AbstractMethod
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_password?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def allow_plaintext?
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def password
|
42
|
+
''
|
43
|
+
end
|
44
|
+
|
45
|
+
def want_anonymous?
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Will be raised by SASL.new_mechanism if mechanism passed to the
|
52
|
+
# constructor is not known.
|
53
|
+
class UnknownMechanism < RuntimeError
|
54
|
+
def initialize(mechanism)
|
55
|
+
@mechanism = mechanism
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
"Unknown mechanism: #{@mechanism.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def SASL.new(mechanisms, preferences)
|
64
|
+
best_mechanism = if preferences.want_anonymous? && mechanisms.include?('ANONYMOUS')
|
65
|
+
'ANONYMOUS'
|
66
|
+
elsif preferences.has_password?
|
67
|
+
if mechanisms.include?('DIGEST-MD5')
|
68
|
+
'DIGEST-MD5'
|
69
|
+
elsif preferences.allow_plaintext?
|
70
|
+
'PLAIN'
|
71
|
+
else
|
72
|
+
raise UnknownMechanism.new(mechanisms)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
raise UnknownMechanism.new(mechanisms)
|
76
|
+
end
|
77
|
+
new_mechanism(best_mechanism, preferences)
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Create a SASL Mechanism for the named mechanism
|
82
|
+
#
|
83
|
+
# mechanism:: [String] mechanism name
|
84
|
+
def SASL.new_mechanism(mechanism, preferences)
|
85
|
+
mechanism_class = case mechanism
|
86
|
+
when 'DIGEST-MD5'
|
87
|
+
DigestMD5
|
88
|
+
when 'PLAIN'
|
89
|
+
Plain
|
90
|
+
when 'ANONYMOUS'
|
91
|
+
Anonymous
|
92
|
+
else
|
93
|
+
raise UnknownMechanism.new(mechanism)
|
94
|
+
end
|
95
|
+
mechanism_class.new(mechanism, preferences)
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
class AbstractMethod < Exception # :nodoc:
|
100
|
+
def to_s
|
101
|
+
"Abstract method is not implemented"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Common functions for mechanisms
|
107
|
+
#
|
108
|
+
# Mechanisms implement handling of methods start and receive. They
|
109
|
+
# return: [message_name, content] or nil where message_name is
|
110
|
+
# either 'auth' or 'response' and content is either a string which
|
111
|
+
# may transmitted encoded as Base64 or nil.
|
112
|
+
class Mechanism
|
113
|
+
attr_reader :mechanism
|
114
|
+
attr_reader :preferences
|
115
|
+
|
116
|
+
def initialize(mechanism, preferences)
|
117
|
+
@mechanism = mechanism
|
118
|
+
@preferences = preferences
|
119
|
+
@state = nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def success?
|
123
|
+
@state == :success
|
124
|
+
end
|
125
|
+
def failure?
|
126
|
+
@state == :failure
|
127
|
+
end
|
128
|
+
|
129
|
+
def start
|
130
|
+
raise AbstractMethod
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def receive(message_name, content)
|
135
|
+
case message_name
|
136
|
+
when 'success'
|
137
|
+
@state = :success
|
138
|
+
when 'failure'
|
139
|
+
@state = :failure
|
140
|
+
end
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/sasl/base64.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# =XMPP4R - XMPP Library for Ruby
|
2
|
+
# License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
|
3
|
+
# Website::http://home.gna.org/xmpp4r/
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'base64'
|
7
|
+
rescue LoadError
|
8
|
+
##
|
9
|
+
# Ruby 1.9 has dropped the Base64 module,
|
10
|
+
# this is a replacement
|
11
|
+
#
|
12
|
+
# We could replace all call by Array#pack('m')
|
13
|
+
# and String#unpack('m'), but this module
|
14
|
+
# improves readability.
|
15
|
+
module Base64
|
16
|
+
##
|
17
|
+
# Encode a String
|
18
|
+
# data:: [String] Binary
|
19
|
+
# result:: [String] Binary in Base64
|
20
|
+
def self.encode64(data)
|
21
|
+
[data].pack('m')
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Decode a Base64-encoded String
|
26
|
+
# data64:: [String] Binary in Base64
|
27
|
+
# result:: [String] Binary
|
28
|
+
def self.decode64(data64)
|
29
|
+
data64.unpack('m').first
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module SASL
|
4
|
+
##
|
5
|
+
# RFC 2831:
|
6
|
+
# http://tools.ietf.org/html/rfc2831
|
7
|
+
class DigestMD5 < Mechanism
|
8
|
+
attr_writer :cnonce
|
9
|
+
|
10
|
+
def initialize(*a)
|
11
|
+
super
|
12
|
+
@nonce_count = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
@state = nil
|
17
|
+
unless defined? @nonce
|
18
|
+
['auth', nil]
|
19
|
+
else
|
20
|
+
# reauthentication
|
21
|
+
receive('challenge', '')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def receive(message_name, content)
|
26
|
+
if message_name == 'challenge'
|
27
|
+
c = decode_challenge(content)
|
28
|
+
|
29
|
+
unless c['rspauth']
|
30
|
+
response = {}
|
31
|
+
if defined?(@nonce) && response['nonce'].nil?
|
32
|
+
# Could be reauth
|
33
|
+
else
|
34
|
+
# No reauth:
|
35
|
+
@nonce_count = 0
|
36
|
+
end
|
37
|
+
@nonce ||= c['nonce']
|
38
|
+
response['nonce'] = @nonce
|
39
|
+
response['charset'] = 'utf-8'
|
40
|
+
response['username'] = preferences.username
|
41
|
+
response['realm'] = c['realm'] || preferences.realm
|
42
|
+
@cnonce = generate_nonce unless defined? @cnonce
|
43
|
+
response['cnonce'] = @cnonce
|
44
|
+
@nc = next_nc
|
45
|
+
response['nc'] = @nc
|
46
|
+
@qop = c['qop'] || 'auth'
|
47
|
+
response['qop'] = @qop
|
48
|
+
response['digest-uri'] = preferences.digest_uri
|
49
|
+
response['response'] = response_value(response['nonce'], response['nc'], response['cnonce'], response['qop'])
|
50
|
+
['response', encode_response(response)]
|
51
|
+
else
|
52
|
+
rspauth_expected = response_value(@nonce, @nc, @cnonce, @qop, '')
|
53
|
+
p :rspauth_received=>c['rspauth'], :rspauth_expected=>rspauth_expected
|
54
|
+
if c['rspauth'] == rspauth_expected
|
55
|
+
['response', nil]
|
56
|
+
else
|
57
|
+
# Bogus server?
|
58
|
+
@state = :failure
|
59
|
+
['failure', nil]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
# No challenge? Might be success or failure
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def decode_challenge(text)
|
71
|
+
challenge = {}
|
72
|
+
|
73
|
+
state = :key
|
74
|
+
key = ''
|
75
|
+
value = ''
|
76
|
+
|
77
|
+
text.scan(/./) do |ch|
|
78
|
+
if state == :key
|
79
|
+
if ch == '='
|
80
|
+
state = :value
|
81
|
+
elsif ch =~ /\S/
|
82
|
+
key += ch
|
83
|
+
end
|
84
|
+
|
85
|
+
elsif state == :value
|
86
|
+
if ch == ','
|
87
|
+
challenge[key] = value
|
88
|
+
key = ''
|
89
|
+
value = ''
|
90
|
+
state = :key
|
91
|
+
elsif ch == '"' and value == ''
|
92
|
+
state = :quote
|
93
|
+
else
|
94
|
+
value += ch
|
95
|
+
end
|
96
|
+
|
97
|
+
elsif state == :quote
|
98
|
+
if ch == '"'
|
99
|
+
state = :value
|
100
|
+
else
|
101
|
+
value += ch
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
challenge[key] = value unless key == ''
|
106
|
+
|
107
|
+
p :decode_challenge => challenge
|
108
|
+
challenge
|
109
|
+
end
|
110
|
+
|
111
|
+
def encode_response(response)
|
112
|
+
p :encode_response => response
|
113
|
+
response.collect do |k,v|
|
114
|
+
if v.include?('"')
|
115
|
+
v.sub!('\\', '\\\\')
|
116
|
+
v.sub!('"', '\\"')
|
117
|
+
"#{k}=\"#{v}\""
|
118
|
+
else
|
119
|
+
"#{k}=#{v}"
|
120
|
+
end
|
121
|
+
end.join(',')
|
122
|
+
end
|
123
|
+
|
124
|
+
def generate_nonce
|
125
|
+
nonce = ''
|
126
|
+
while nonce.length < 16
|
127
|
+
c = rand(128).chr
|
128
|
+
nonce += c if c =~ /^[a-zA-Z0-9]$/
|
129
|
+
end
|
130
|
+
nonce
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Function from RFC2831
|
135
|
+
def h(s); Digest::MD5.digest(s); end
|
136
|
+
##
|
137
|
+
# Function from RFC2831
|
138
|
+
def hh(s); Digest::MD5.hexdigest(s); end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Calculate the value for the response field
|
142
|
+
def response_value(nonce, nc, cnonce, qop, a2_prefix='AUTHENTICATE')
|
143
|
+
p :response_value => {:nonce=>nonce,
|
144
|
+
:cnonce=>cnonce,
|
145
|
+
:qop=>qop,
|
146
|
+
:username=>preferences.username,
|
147
|
+
:realm=>preferences.realm,
|
148
|
+
:password=>preferences.password,
|
149
|
+
:authzid=>preferences.authzid}
|
150
|
+
a1_h = h("#{preferences.username}:#{preferences.realm}:#{preferences.password}")
|
151
|
+
a1 = "#{a1_h}:#{nonce}:#{cnonce}"
|
152
|
+
if preferences.authzid
|
153
|
+
a1 += ":#{preferences.authzid}"
|
154
|
+
end
|
155
|
+
if qop && (qop.downcase == 'auth-int' || qop.downcase == 'auth-conf')
|
156
|
+
a2 = "#{a2_prefix}:#{preferences.digest_uri}:00000000000000000000000000000000"
|
157
|
+
else
|
158
|
+
a2 = "#{a2_prefix}:#{preferences.digest_uri}"
|
159
|
+
end
|
160
|
+
hh("#{hh(a1)}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hh(a2)}")
|
161
|
+
end
|
162
|
+
|
163
|
+
def next_nc
|
164
|
+
@nonce_count += 1
|
165
|
+
s = @nonce_count.to_s
|
166
|
+
s = "0#{s}" while s.length < 8
|
167
|
+
s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
data/lib/sasl/plain.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module SASL
|
2
|
+
##
|
3
|
+
# RFC 4616:
|
4
|
+
# http://tools.ietf.org/html/rfc4616
|
5
|
+
class Plain < Mechanism
|
6
|
+
def start
|
7
|
+
@state = nil
|
8
|
+
message = [preferences.authzid.to_s,
|
9
|
+
preferences.username,
|
10
|
+
preferences.password].join("\000")
|
11
|
+
['auth', message]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sasl'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
describe SASL::Anonymous do
|
5
|
+
class MyAnonymousPreferences < SASL::Preferences
|
6
|
+
def username
|
7
|
+
'bob'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
preferences = MyAnonymousPreferences.new
|
11
|
+
|
12
|
+
it 'should authenticate anonymously' do
|
13
|
+
sasl = SASL::Anonymous.new('ANONYMOUS', preferences)
|
14
|
+
sasl.start.should == ['auth', 'bob']
|
15
|
+
sasl.success?.should == false
|
16
|
+
sasl.receive('success', nil).should == nil
|
17
|
+
sasl.success?.should == true
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'sasl'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
describe SASL::DigestMD5 do
|
5
|
+
# Preferences from http://tools.ietf.org/html/rfc2831#section-4
|
6
|
+
class MyDigestMD5Preferences < SASL::Preferences
|
7
|
+
attr_writer :serv_type
|
8
|
+
def realm
|
9
|
+
'elwood.innosoft.com'
|
10
|
+
end
|
11
|
+
def digest_uri
|
12
|
+
"#{@serv_type}/elwood.innosoft.com"
|
13
|
+
end
|
14
|
+
def username
|
15
|
+
'chris'
|
16
|
+
end
|
17
|
+
def has_password?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
def password
|
21
|
+
'secret'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
preferences = MyDigestMD5Preferences.new
|
25
|
+
|
26
|
+
it 'should authenticate (1)' do
|
27
|
+
preferences.serv_type = 'imap'
|
28
|
+
sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences)
|
29
|
+
sasl.start.should == ['auth', nil]
|
30
|
+
sasl.cnonce = 'OA6MHXh6VqTrRk'
|
31
|
+
response = sasl.receive('challenge',
|
32
|
+
'realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
|
33
|
+
algorithm=md5-sess,charset=utf-8')
|
34
|
+
response[0].should == 'response'
|
35
|
+
response[1].should =~ /charset="?utf-8"?/
|
36
|
+
response[1].should =~ /username="?chris"?/
|
37
|
+
response[1].should =~ /realm="?elwood.innosoft.com"?/
|
38
|
+
response[1].should =~ /nonce="?OA6MG9tEQGm2hh"?/
|
39
|
+
response[1].should =~ /nc="?00000001"?/
|
40
|
+
response[1].should =~ /cnonce="?OA6MHXh6VqTrRk"?/
|
41
|
+
response[1].should =~ /digest-uri="?imap\/elwood.innosoft.com"?/
|
42
|
+
response[1].should =~ /response=d388dad90d4bbd760a152321f2143af7"?/
|
43
|
+
response[1].should =~ /"?qop=auth"?/
|
44
|
+
|
45
|
+
sasl.receive('challenge',
|
46
|
+
'rspauth=ea40f60335c427b5527b84dbabcdfffd').should ==
|
47
|
+
['response', nil]
|
48
|
+
sasl.receive('success', nil).should == nil
|
49
|
+
sasl.success?.should == true
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should authenticate (2)' do
|
53
|
+
preferences.serv_type = 'acap'
|
54
|
+
sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences)
|
55
|
+
sasl.start.should == ['auth', nil]
|
56
|
+
sasl.cnonce = 'OA9BSuZWMSpW8m'
|
57
|
+
response = sasl.receive('challenge',
|
58
|
+
'realm="elwood.innosoft.com",nonce="OA9BSXrbuRhWay",qop="auth",
|
59
|
+
algorithm=md5-sess,charset=utf-8')
|
60
|
+
response[0].should == 'response'
|
61
|
+
response[1].should =~ /charset="?utf-8"?/
|
62
|
+
response[1].should =~ /username="?chris"?/
|
63
|
+
response[1].should =~ /realm="?elwood.innosoft.com"?/
|
64
|
+
response[1].should =~ /nonce="?OA9BSXrbuRhWay"?/
|
65
|
+
response[1].should =~ /nc="?00000001"?/
|
66
|
+
response[1].should =~ /cnonce="?OA9BSuZWMSpW8m"?/
|
67
|
+
response[1].should =~ /digest-uri="?acap\/elwood.innosoft.com"?/
|
68
|
+
response[1].should =~ /response=6084c6db3fede7352c551284490fd0fc"?/
|
69
|
+
response[1].should =~ /"?qop=auth"?/
|
70
|
+
|
71
|
+
sasl.receive('challenge',
|
72
|
+
'rspauth=2f0b3d7c3c2e486600ef710726aa2eae').should ==
|
73
|
+
['response', nil]
|
74
|
+
sasl.receive('success', nil).should == nil
|
75
|
+
sasl.success?.should == true
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should reauthenticate' do
|
79
|
+
preferences.serv_type = 'imap'
|
80
|
+
sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences)
|
81
|
+
sasl.start.should == ['auth', nil]
|
82
|
+
sasl.cnonce = 'OA6MHXh6VqTrRk'
|
83
|
+
sasl.receive('challenge',
|
84
|
+
'realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
|
85
|
+
algorithm=md5-sess,charset=utf-8')
|
86
|
+
# reauth:
|
87
|
+
response = sasl.start
|
88
|
+
response[0].should == 'response'
|
89
|
+
response[1].should =~ /charset="?utf-8"?/
|
90
|
+
response[1].should =~ /username="?chris"?/
|
91
|
+
response[1].should =~ /realm="?elwood.innosoft.com"?/
|
92
|
+
response[1].should =~ /nonce="?OA6MG9tEQGm2hh"?/
|
93
|
+
response[1].should =~ /nc="?00000002"?/
|
94
|
+
response[1].should =~ /cnonce="?OA6MHXh6VqTrRk"?/
|
95
|
+
response[1].should =~ /digest-uri="?imap\/elwood.innosoft.com"?/
|
96
|
+
response[1].should =~ /response=b0b5d72a400655b8306e434566b10efb"?/ # my own result
|
97
|
+
response[1].should =~ /"?qop=auth"?/
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should fail' do
|
101
|
+
sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences)
|
102
|
+
sasl.start.should == ['auth', nil]
|
103
|
+
sasl.receive('failure', 'EPIC FAIL')
|
104
|
+
sasl.failure?.should == true
|
105
|
+
sasl.success?.should == false
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'sasl'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
describe SASL do
|
5
|
+
it 'should know DIGEST-MD5' do
|
6
|
+
sasl = SASL.new_mechanism('DIGEST-MD5', SASL::Preferences.new)
|
7
|
+
sasl.should be_an_instance_of SASL::DigestMD5
|
8
|
+
end
|
9
|
+
it 'should know PLAIN' do
|
10
|
+
sasl = SASL.new_mechanism('PLAIN', SASL::Preferences.new)
|
11
|
+
sasl.should be_an_instance_of SASL::Plain
|
12
|
+
end
|
13
|
+
it 'should know ANONYMOUS' do
|
14
|
+
sasl = SASL.new_mechanism('ANONYMOUS', SASL::Preferences.new)
|
15
|
+
sasl.should be_an_instance_of SASL::Anonymous
|
16
|
+
end
|
17
|
+
it 'should choose ANONYMOUS' do
|
18
|
+
preferences = SASL::Preferences.new
|
19
|
+
class << preferences
|
20
|
+
def want_anonymous?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
SASL.new(%w(PLAIN DIGEST-MD5 ANONYMOUS), preferences).should be_an_instance_of SASL::Anonymous
|
25
|
+
end
|
26
|
+
it 'should choose DIGEST-MD5' do
|
27
|
+
preferences = SASL::Preferences.new
|
28
|
+
class << preferences
|
29
|
+
def has_password?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
SASL.new(%w(PLAIN DIGEST-MD5 ANONYMOUS), preferences).should be_an_instance_of SASL::DigestMD5
|
34
|
+
end
|
35
|
+
it 'should choose PLAIN' do
|
36
|
+
preferences = SASL::Preferences.new
|
37
|
+
class << preferences
|
38
|
+
def has_password?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
def allow_plaintext?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
SASL.new(%w(PLAIN ANONYMOUS), preferences).should be_an_instance_of SASL::Plain
|
46
|
+
end
|
47
|
+
it 'should disallow PLAIN by default' do
|
48
|
+
preferences = SASL::Preferences.new
|
49
|
+
class << preferences
|
50
|
+
def has_password?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
lambda { SASL.new(%w(PLAIN ANONYMOUS), preferences) }.should raise_error(SASL::UnknownMechanism)
|
55
|
+
end
|
56
|
+
end
|
data/spec/plain_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'sasl'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
describe SASL::Plain do
|
5
|
+
class MyPlainPreferences < SASL::Preferences
|
6
|
+
def authzid
|
7
|
+
'bob@example.com'
|
8
|
+
end
|
9
|
+
def username
|
10
|
+
'bob'
|
11
|
+
end
|
12
|
+
def has_password?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
def password
|
16
|
+
's3cr3t'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
preferences = MyPlainPreferences.new
|
20
|
+
|
21
|
+
it 'should authenticate' do
|
22
|
+
sasl = SASL::Plain.new('PLAIN', preferences)
|
23
|
+
sasl.start.should == ['auth', "bob@example.com\000bob\000s3cr3t"]
|
24
|
+
sasl.success?.should == false
|
25
|
+
sasl.receive('success', nil).should == nil
|
26
|
+
sasl.failure?.should == false
|
27
|
+
sasl.success?.should == true
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should recognize failure' do
|
31
|
+
sasl = SASL::Plain.new('PLAIN', preferences)
|
32
|
+
sasl.start.should == ['auth', "bob@example.com\000bob\000s3cr3t"]
|
33
|
+
sasl.success?.should == false
|
34
|
+
sasl.failure?.should == false
|
35
|
+
sasl.receive('failure', 'keep-idiots-out').should == nil
|
36
|
+
sasl.failure?.should == true
|
37
|
+
sasl.success?.should == false
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-sasl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephan Maka
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-07 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Simple Authentication and Security Layer (RFC 4422)
|
17
|
+
email: stephan@spaceboyz.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- spec/mechanism_spec.rb
|
26
|
+
- spec/anonymous_spec.rb
|
27
|
+
- spec/plain_spec.rb
|
28
|
+
- spec/digest_md5_spec.rb
|
29
|
+
- lib/sasl/base.rb
|
30
|
+
- lib/sasl/digest_md5.rb
|
31
|
+
- lib/sasl/anonymous.rb
|
32
|
+
- lib/sasl/plain.rb
|
33
|
+
- lib/sasl/base64.rb
|
34
|
+
- lib/sasl.rb
|
35
|
+
- README.markdown
|
36
|
+
has_rdoc: false
|
37
|
+
homepage: http://github.com/astro/ruby-sasl/
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.1
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: SASL client library
|
62
|
+
test_files:
|
63
|
+
- spec/mechanism_spec.rb
|
64
|
+
- spec/anonymous_spec.rb
|
65
|
+
- spec/plain_spec.rb
|
66
|
+
- spec/digest_md5_spec.rb
|