astro-ruby-sasl 0.0.2
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/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 +168 -0
- data/lib/sasl/plain.rb +14 -0
- data/spec/anonymous_spec.rb +19 -0
- data/spec/digest_md5_spec.rb +97 -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,168 @@
|
|
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
|
+
@state = :success
|
56
|
+
['success', nil]
|
57
|
+
else
|
58
|
+
@state = :failure
|
59
|
+
['failure', nil]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def decode_challenge(text)
|
68
|
+
challenge = {}
|
69
|
+
|
70
|
+
state = :key
|
71
|
+
key = ''
|
72
|
+
value = ''
|
73
|
+
|
74
|
+
text.scan(/./) do |ch|
|
75
|
+
if state == :key
|
76
|
+
if ch == '='
|
77
|
+
state = :value
|
78
|
+
elsif ch =~ /\S/
|
79
|
+
key += ch
|
80
|
+
end
|
81
|
+
|
82
|
+
elsif state == :value
|
83
|
+
if ch == ','
|
84
|
+
challenge[key] = value
|
85
|
+
key = ''
|
86
|
+
value = ''
|
87
|
+
state = :key
|
88
|
+
elsif ch == '"' and value == ''
|
89
|
+
state = :quote
|
90
|
+
else
|
91
|
+
value += ch
|
92
|
+
end
|
93
|
+
|
94
|
+
elsif state == :quote
|
95
|
+
if ch == '"'
|
96
|
+
state = :value
|
97
|
+
else
|
98
|
+
value += ch
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
challenge[key] = value unless key == ''
|
103
|
+
|
104
|
+
p :decode_challenge => challenge
|
105
|
+
challenge
|
106
|
+
end
|
107
|
+
|
108
|
+
def encode_response(response)
|
109
|
+
p :encode_response => response
|
110
|
+
response.collect do |k,v|
|
111
|
+
if v.include?('"')
|
112
|
+
v.sub!('\\', '\\\\')
|
113
|
+
v.sub!('"', '\\"')
|
114
|
+
"#{k}=\"#{v}\""
|
115
|
+
else
|
116
|
+
"#{k}=#{v}"
|
117
|
+
end
|
118
|
+
end.join(',')
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate_nonce
|
122
|
+
nonce = ''
|
123
|
+
while nonce.length < 16
|
124
|
+
c = rand(128).chr
|
125
|
+
nonce += c if c =~ /^[a-zA-Z0-9]$/
|
126
|
+
end
|
127
|
+
nonce
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Function from RFC2831
|
132
|
+
def h(s); Digest::MD5.digest(s); end
|
133
|
+
##
|
134
|
+
# Function from RFC2831
|
135
|
+
def hh(s); Digest::MD5.hexdigest(s); end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Calculate the value for the response field
|
139
|
+
def response_value(nonce, nc, cnonce, qop, a2_prefix='AUTHENTICATE')
|
140
|
+
p :response_value => {:nonce=>nonce,
|
141
|
+
:cnonce=>cnonce,
|
142
|
+
:qop=>qop,
|
143
|
+
:username=>preferences.username,
|
144
|
+
:realm=>preferences.realm,
|
145
|
+
:password=>preferences.password,
|
146
|
+
:authzid=>preferences.authzid}
|
147
|
+
a1_h = h("#{preferences.username}:#{preferences.realm}:#{preferences.password}")
|
148
|
+
a1 = "#{a1_h}:#{nonce}:#{cnonce}"
|
149
|
+
if preferences.authzid
|
150
|
+
a1 += ":#{preferences.authzid}"
|
151
|
+
end
|
152
|
+
if qop && (qop.downcase == 'auth-int' || qop.downcase == 'auth-conf')
|
153
|
+
a2 = "#{a2_prefix}:#{preferences.digest_uri}:00000000000000000000000000000000"
|
154
|
+
else
|
155
|
+
a2 = "#{a2_prefix}:#{preferences.digest_uri}"
|
156
|
+
end
|
157
|
+
hh("#{hh(a1)}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hh(a2)}")
|
158
|
+
end
|
159
|
+
|
160
|
+
def next_nc
|
161
|
+
@nonce_count += 1
|
162
|
+
s = @nonce_count.to_s
|
163
|
+
s = "0#{s}" while s.length < 8
|
164
|
+
s
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
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,97 @@
|
|
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
|
+
['success', nil]
|
48
|
+
sasl.success?.should == true
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should authenticate (2)' do
|
52
|
+
preferences.serv_type = 'acap'
|
53
|
+
sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences)
|
54
|
+
sasl.start.should == ['auth', nil]
|
55
|
+
sasl.cnonce = 'OA9BSuZWMSpW8m'
|
56
|
+
response = sasl.receive('challenge',
|
57
|
+
'realm="elwood.innosoft.com",nonce="OA9BSXrbuRhWay",qop="auth",
|
58
|
+
algorithm=md5-sess,charset=utf-8')
|
59
|
+
response[0].should == 'response'
|
60
|
+
response[1].should =~ /charset="?utf-8"?/
|
61
|
+
response[1].should =~ /username="?chris"?/
|
62
|
+
response[1].should =~ /realm="?elwood.innosoft.com"?/
|
63
|
+
response[1].should =~ /nonce="?OA9BSXrbuRhWay"?/
|
64
|
+
response[1].should =~ /nc="?00000001"?/
|
65
|
+
response[1].should =~ /cnonce="?OA9BSuZWMSpW8m"?/
|
66
|
+
response[1].should =~ /digest-uri="?acap\/elwood.innosoft.com"?/
|
67
|
+
response[1].should =~ /response=6084c6db3fede7352c551284490fd0fc"?/
|
68
|
+
response[1].should =~ /"?qop=auth"?/
|
69
|
+
|
70
|
+
sasl.receive('challenge',
|
71
|
+
'rspauth=2f0b3d7c3c2e486600ef710726aa2eae').should ==
|
72
|
+
['success', nil]
|
73
|
+
sasl.success?.should == true
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should reauthenticate' do
|
77
|
+
preferences.serv_type = 'imap'
|
78
|
+
sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences)
|
79
|
+
sasl.start.should == ['auth', nil]
|
80
|
+
sasl.cnonce = 'OA6MHXh6VqTrRk'
|
81
|
+
sasl.receive('challenge',
|
82
|
+
'realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
|
83
|
+
algorithm=md5-sess,charset=utf-8')
|
84
|
+
# reauth:
|
85
|
+
response = sasl.start
|
86
|
+
response[0].should == 'response'
|
87
|
+
response[1].should =~ /charset="?utf-8"?/
|
88
|
+
response[1].should =~ /username="?chris"?/
|
89
|
+
response[1].should =~ /realm="?elwood.innosoft.com"?/
|
90
|
+
response[1].should =~ /nonce="?OA6MG9tEQGm2hh"?/
|
91
|
+
response[1].should =~ /nc="?00000002"?/
|
92
|
+
response[1].should =~ /cnonce="?OA6MHXh6VqTrRk"?/
|
93
|
+
response[1].should =~ /digest-uri="?imap\/elwood.innosoft.com"?/
|
94
|
+
response[1].should =~ /response=b0b5d72a400655b8306e434566b10efb"?/ # my own result
|
95
|
+
response[1].should =~ /"?qop=auth"?/
|
96
|
+
end
|
97
|
+
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: astro-ruby-sasl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephan Maka
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-06 00:00:00 -08: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.2.0
|
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
|