net-imap 0.3.8 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of net-imap might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +46 -0
- data/.github/workflows/test.yml +5 -12
- data/Gemfile +1 -0
- data/README.md +15 -4
- data/Rakefile +0 -7
- data/benchmarks/generate_parser_benchmarks +52 -0
- data/benchmarks/parser.yml +578 -0
- data/benchmarks/stringprep.yml +1 -1
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +13 -6
- data/lib/net/imap/deprecated_client_options.rb +139 -0
- data/lib/net/imap/response_data.rb +46 -41
- data/lib/net/imap/response_parser/parser_utils.rb +230 -0
- data/lib/net/imap/response_parser.rb +667 -649
- data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
- data/lib/net/imap/sasl/authenticators.rb +112 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
- data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
- data/lib/net/imap/sasl/external_authenticator.rb +62 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
- data/lib/net/imap/sasl.rb +139 -44
- data/lib/net/imap/stringprep/nameprep.rb +70 -0
- data/lib/net/imap/stringprep/saslprep.rb +69 -0
- data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
- data/lib/net/imap/stringprep/tables.rb +146 -0
- data/lib/net/imap/stringprep/trace.rb +85 -0
- data/lib/net/imap/stringprep.rb +159 -0
- data/lib/net/imap.rb +967 -588
- data/net-imap.gemspec +1 -1
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +82 -60
- metadata +30 -12
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/sasl/saslprep.rb +0 -55
- data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
- data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
module SASL
|
6
|
+
|
7
|
+
# Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by
|
8
|
+
# RFC-4505[https://tools.ietf.org/html/rfc4505]. See
|
9
|
+
# Net::IMAP#authenticate.
|
10
|
+
class AnonymousAuthenticator
|
11
|
+
|
12
|
+
# An optional token sent for the +ANONYMOUS+ mechanism., up to 255 UTF-8
|
13
|
+
# characters in length.
|
14
|
+
#
|
15
|
+
# If it contains an "@" sign, the message must be a valid email address
|
16
|
+
# (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
|
17
|
+
# Email syntax is _not_ validated by AnonymousAuthenticator.
|
18
|
+
#
|
19
|
+
# Otherwise, it can be any UTF8 string which is permitted by the
|
20
|
+
# StringPrep::Trace profile.
|
21
|
+
attr_reader :anonymous_message
|
22
|
+
|
23
|
+
# :call-seq:
|
24
|
+
# new(anonymous_message = "", **) -> authenticator
|
25
|
+
# new(anonymous_message: "", **) -> authenticator
|
26
|
+
#
|
27
|
+
# Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
|
28
|
+
# specified in RFC-4505[https://tools.ietf.org/html/rfc4505]. To use
|
29
|
+
# this, see Net::IMAP#authenticate or your client's authentication
|
30
|
+
# method.
|
31
|
+
#
|
32
|
+
# #anonymous_message is an optional message which is sent to the server.
|
33
|
+
# It may be sent as a positional argument or as a keyword argument.
|
34
|
+
#
|
35
|
+
# Any other keyword arguments are silently ignored.
|
36
|
+
def initialize(anon_msg = nil, anonymous_message: nil, **)
|
37
|
+
message = (anonymous_message || anon_msg || "").to_str
|
38
|
+
@anonymous_message = StringPrep::Trace.stringprep_trace message
|
39
|
+
if (size = @anonymous_message&.length)&.> 255
|
40
|
+
raise ArgumentError,
|
41
|
+
"anonymous_message is too long. (%d codepoints)" % [size]
|
42
|
+
end
|
43
|
+
@done = false
|
44
|
+
end
|
45
|
+
|
46
|
+
# :call-seq:
|
47
|
+
# initial_response? -> true
|
48
|
+
#
|
49
|
+
# +ANONYMOUS+ can send an initial client response.
|
50
|
+
def initial_response?; true end
|
51
|
+
|
52
|
+
# Returns #anonymous_message.
|
53
|
+
def process(_server_challenge_string)
|
54
|
+
anonymous_message
|
55
|
+
ensure
|
56
|
+
@done = true
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true when the initial client response was sent.
|
60
|
+
#
|
61
|
+
# The authentication should not succeed unless this returns true, but it
|
62
|
+
# does *not* indicate success.
|
63
|
+
def done?; @done end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net::IMAP::SASL
|
4
|
+
|
5
|
+
# Registry for SASL authenticators
|
6
|
+
#
|
7
|
+
# Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or
|
8
|
+
# a proc), receiving any credentials and options and returning an
|
9
|
+
# authenticator instance. The returned object represents a single
|
10
|
+
# authentication exchange and <em>must not</em> be reused for multiple
|
11
|
+
# authentication attempts.
|
12
|
+
#
|
13
|
+
# An authenticator instance object must respond to +#process+, receiving the
|
14
|
+
# server's challenge and returning the client's response. Optionally, it may
|
15
|
+
# also respond to +#initial_response?+ and +#done?+. When
|
16
|
+
# +#initial_response?+ returns +true+, +#process+ may be called the first
|
17
|
+
# time with +nil+. When +#done?+ returns +false+, the exchange is incomplete
|
18
|
+
# and an exception should be raised if the exchange terminates prematurely.
|
19
|
+
#
|
20
|
+
# See the source for PlainAuthenticator, XOAuth2Authenticator, and
|
21
|
+
# ScramSHA1Authenticator for examples.
|
22
|
+
class Authenticators
|
23
|
+
|
24
|
+
# Create a new Authenticators registry.
|
25
|
+
#
|
26
|
+
# This class is usually not instantiated directly. Use SASL.authenticators
|
27
|
+
# to reuse the default global registry.
|
28
|
+
#
|
29
|
+
# When +use_defaults+ is +false+, the registry will start empty. When
|
30
|
+
# +use_deprecated+ is +false+, deprecated authenticators will not be
|
31
|
+
# included with the defaults.
|
32
|
+
def initialize(use_defaults: true, use_deprecated: true)
|
33
|
+
@authenticators = {}
|
34
|
+
return unless use_defaults
|
35
|
+
add_authenticator "Anonymous"
|
36
|
+
add_authenticator "External"
|
37
|
+
add_authenticator "OAuthBearer"
|
38
|
+
add_authenticator "Plain"
|
39
|
+
add_authenticator "Scram-SHA-1"
|
40
|
+
add_authenticator "Scram-SHA-256"
|
41
|
+
add_authenticator "XOAuth2"
|
42
|
+
return unless use_deprecated
|
43
|
+
add_authenticator "Login" # deprecated
|
44
|
+
add_authenticator "Cram-MD5" # deprecated
|
45
|
+
add_authenticator "Digest-MD5" # deprecated
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the names of all registered SASL mechanisms.
|
49
|
+
def names; @authenticators.keys end
|
50
|
+
|
51
|
+
# :call-seq:
|
52
|
+
# add_authenticator(mechanism)
|
53
|
+
# add_authenticator(mechanism, authenticator_class)
|
54
|
+
# add_authenticator(mechanism, authenticator_proc)
|
55
|
+
#
|
56
|
+
# Registers an authenticator for #authenticator to use. +mechanism+ is the
|
57
|
+
# name of the
|
58
|
+
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
|
59
|
+
# implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
|
60
|
+
#
|
61
|
+
# If +mechanism+ refers to an existing authenticator, a warning will be
|
62
|
+
# printed and the old authenticator will be replaced.
|
63
|
+
#
|
64
|
+
# When only a single argument is given, the authenticator class will be
|
65
|
+
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
|
66
|
+
# preserved and non-alphanumeric characters are removed..
|
67
|
+
def add_authenticator(name, authenticator = nil)
|
68
|
+
key = name.upcase.to_sym
|
69
|
+
authenticator ||= begin
|
70
|
+
class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
|
71
|
+
auth_class = nil
|
72
|
+
->(*creds, **props, &block) {
|
73
|
+
auth_class ||= Net::IMAP::SASL.const_get(class_name)
|
74
|
+
auth_class.new(*creds, **props, &block)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
@authenticators[key] = authenticator
|
78
|
+
end
|
79
|
+
|
80
|
+
# Removes the authenticator registered for +name+
|
81
|
+
def remove_authenticator(name)
|
82
|
+
key = name.upcase.to_sym
|
83
|
+
@authenticators.delete(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
# :call-seq:
|
87
|
+
# authenticator(mechanism, ...) -> auth_session
|
88
|
+
#
|
89
|
+
# Builds an authenticator instance using the authenticator registered to
|
90
|
+
# +mechanism+. The returned object represents a single authentication
|
91
|
+
# exchange and <em>must not</em> be reused for multiple authentication
|
92
|
+
# attempts.
|
93
|
+
#
|
94
|
+
# All arguments (except +mechanism+) are forwarded to the registered
|
95
|
+
# authenticator's +#new+ or +#call+ method. Each authenticator must
|
96
|
+
# document its own arguments.
|
97
|
+
#
|
98
|
+
# [Note]
|
99
|
+
# This method is intended for internal use by connection protocol code
|
100
|
+
# only. Protocol client users should see refer to their client's
|
101
|
+
# documentation, e.g. Net::IMAP#authenticate.
|
102
|
+
def authenticator(mechanism, ...)
|
103
|
+
auth = @authenticators.fetch(mechanism.upcase.to_sym) do
|
104
|
+
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
|
105
|
+
end
|
106
|
+
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
|
107
|
+
end
|
108
|
+
alias new authenticator
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -13,14 +13,7 @@
|
|
13
13
|
# Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
|
14
14
|
# of cleartext and recommends TLS version 1.2 or greater be used for all
|
15
15
|
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
|
16
|
-
class Net::IMAP::CramMD5Authenticator
|
17
|
-
def process(challenge)
|
18
|
-
digest = hmac_md5(challenge, @password)
|
19
|
-
return @user + " " + digest
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
16
|
+
class Net::IMAP::SASL::CramMD5Authenticator
|
24
17
|
def initialize(user, password, warn_deprecation: true, **_ignored)
|
25
18
|
if warn_deprecation
|
26
19
|
warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
|
@@ -28,8 +21,22 @@ class Net::IMAP::CramMD5Authenticator
|
|
28
21
|
require "digest/md5"
|
29
22
|
@user = user
|
30
23
|
@password = password
|
24
|
+
@done = false
|
25
|
+
end
|
26
|
+
|
27
|
+
def initial_response?; false end
|
28
|
+
|
29
|
+
def process(challenge)
|
30
|
+
digest = hmac_md5(challenge, @password)
|
31
|
+
return @user + " " + digest
|
32
|
+
ensure
|
33
|
+
@done = true
|
31
34
|
end
|
32
35
|
|
36
|
+
def done?; @done end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
33
40
|
def hmac_md5(text, key)
|
34
41
|
if key.length > 64
|
35
42
|
key = Digest::MD5.digest(key)
|
@@ -47,5 +54,4 @@ class Net::IMAP::CramMD5Authenticator
|
|
47
54
|
return Digest::MD5.hexdigest(k_opad + digest)
|
48
55
|
end
|
49
56
|
|
50
|
-
Net::IMAP.add_authenticator "CRAM-MD5", self
|
51
57
|
end
|
@@ -1,14 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
|
4
|
-
# in
|
4
|
+
# in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
|
5
5
|
#
|
6
6
|
# == Deprecated
|
7
7
|
#
|
8
8
|
# "+DIGEST-MD5+" has been deprecated by
|
9
|
-
#
|
9
|
+
# RFC-6331[https://tools.ietf.org/html/rfc6331] and should not be relied on for
|
10
10
|
# security. It is included for compatibility with existing servers.
|
11
|
-
class Net::IMAP::DigestMD5Authenticator
|
11
|
+
class Net::IMAP::SASL::DigestMD5Authenticator
|
12
|
+
STAGE_ONE = :stage_one
|
13
|
+
STAGE_TWO = :stage_two
|
14
|
+
STAGE_DONE = :stage_done
|
15
|
+
private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
|
16
|
+
|
17
|
+
# Authentication identity: the identity that matches the #password.
|
18
|
+
#
|
19
|
+
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
|
20
|
+
# "Authentication identity" is the generic term used by
|
21
|
+
# RFC-4422[https://tools.ietf.org/html/rfc4422].
|
22
|
+
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
|
23
|
+
# that to +authcid+. So +authcid+ is available as an alias for #username.
|
24
|
+
attr_reader :username
|
25
|
+
|
26
|
+
# A password or passphrase that matches the #username.
|
27
|
+
#
|
28
|
+
# The +password+ will be used to create the response digest.
|
29
|
+
attr_reader :password
|
30
|
+
|
31
|
+
# Authorization identity: an identity to act as or on behalf of. The identity
|
32
|
+
# form is application protocol specific. If not provided or left blank, the
|
33
|
+
# server derives an authorization identity from the authentication identity.
|
34
|
+
# The server is responsible for verifying the client's credentials and
|
35
|
+
# verifying that the identity it associates with the client's authentication
|
36
|
+
# identity is allowed to act as (or on behalf of) the authorization identity.
|
37
|
+
#
|
38
|
+
# For example, an administrator or superuser might take on another role:
|
39
|
+
#
|
40
|
+
# imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
|
41
|
+
#
|
42
|
+
attr_reader :authzid
|
43
|
+
|
44
|
+
# :call-seq:
|
45
|
+
# new(username, password, authzid = nil, **options) -> authenticator
|
46
|
+
# new(username:, password:, authzid: nil, **options) -> authenticator
|
47
|
+
#
|
48
|
+
# Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
|
49
|
+
#
|
50
|
+
# Called by Net::IMAP#authenticate and similar methods on other clients.
|
51
|
+
#
|
52
|
+
# ==== Parameters
|
53
|
+
#
|
54
|
+
# * #username — Identity whose #password is used.
|
55
|
+
# * #password — A password or passphrase associated with this #username.
|
56
|
+
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
|
57
|
+
# * +warn_deprecation+ — Set to +false+ to silence the warning.
|
58
|
+
#
|
59
|
+
# See the documentation for each attribute for more details.
|
60
|
+
def initialize(user = nil, pass = nil, authz = nil,
|
61
|
+
username: nil, password: nil, authzid: nil,
|
62
|
+
warn_deprecation: true, **)
|
63
|
+
username ||= user or raise ArgumentError, "missing username"
|
64
|
+
password ||= pass or raise ArgumentError, "missing password"
|
65
|
+
authzid ||= authz
|
66
|
+
if warn_deprecation
|
67
|
+
warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
|
68
|
+
# TODO: recommend SCRAM instead.
|
69
|
+
end
|
70
|
+
require "digest/md5"
|
71
|
+
require "strscan"
|
72
|
+
@username, @password, @authzid = username, password, authzid
|
73
|
+
@nc, @stage = {}, STAGE_ONE
|
74
|
+
end
|
75
|
+
|
76
|
+
def initial_response?; false end
|
77
|
+
|
78
|
+
# Responds to server challenge in two stages.
|
12
79
|
def process(challenge)
|
13
80
|
case @stage
|
14
81
|
when STAGE_ONE
|
@@ -31,7 +98,7 @@ class Net::IMAP::DigestMD5Authenticator
|
|
31
98
|
|
32
99
|
response = {
|
33
100
|
:nonce => sparams['nonce'],
|
34
|
-
:username => @
|
101
|
+
:username => @username,
|
35
102
|
:realm => sparams['realm'],
|
36
103
|
:cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
|
37
104
|
:'digest-uri' => 'imap/' + sparams['realm'],
|
@@ -41,7 +108,7 @@ class Net::IMAP::DigestMD5Authenticator
|
|
41
108
|
:charset => sparams['charset'],
|
42
109
|
}
|
43
110
|
|
44
|
-
response[:authzid] = @
|
111
|
+
response[:authzid] = @authzid unless @authzid.nil?
|
45
112
|
|
46
113
|
# now, the real thing
|
47
114
|
a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
|
@@ -62,7 +129,7 @@ class Net::IMAP::DigestMD5Authenticator
|
|
62
129
|
|
63
130
|
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
|
64
131
|
when STAGE_TWO
|
65
|
-
@stage =
|
132
|
+
@stage = STAGE_DONE
|
66
133
|
# if at the second stage, return an empty string
|
67
134
|
if challenge =~ /rspauth=/
|
68
135
|
return ''
|
@@ -74,23 +141,10 @@ class Net::IMAP::DigestMD5Authenticator
|
|
74
141
|
end
|
75
142
|
end
|
76
143
|
|
77
|
-
def
|
78
|
-
if warn_deprecation
|
79
|
-
warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
|
80
|
-
# TODO: recommend SCRAM instead.
|
81
|
-
end
|
82
|
-
require "digest/md5"
|
83
|
-
require "strscan"
|
84
|
-
@user, @password, @authname = user, password, authname
|
85
|
-
@nc, @stage = {}, STAGE_ONE
|
86
|
-
end
|
87
|
-
|
144
|
+
def done?; @stage == STAGE_DONE end
|
88
145
|
|
89
146
|
private
|
90
147
|
|
91
|
-
STAGE_ONE = :stage_one
|
92
|
-
STAGE_TWO = :stage_two
|
93
|
-
|
94
148
|
def nc(nonce)
|
95
149
|
if @nc.has_key? nonce
|
96
150
|
@nc[nonce] = @nc[nonce] + 1
|
@@ -111,5 +165,4 @@ class Net::IMAP::DigestMD5Authenticator
|
|
111
165
|
end
|
112
166
|
end
|
113
167
|
|
114
|
-
Net::IMAP.add_authenticator "DIGEST-MD5", self
|
115
168
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
module SASL
|
6
|
+
|
7
|
+
# Authenticator for the "+EXTERNAL+" SASL mechanism, as specified by
|
8
|
+
# RFC-4422[https://tools.ietf.org/html/rfc4422]. See
|
9
|
+
# Net::IMAP#authenticate.
|
10
|
+
#
|
11
|
+
# The EXTERNAL mechanism requests that the server use client credentials
|
12
|
+
# established external to SASL, for example by TLS certificate or IPsec.
|
13
|
+
class ExternalAuthenticator
|
14
|
+
|
15
|
+
# Authorization identity: an identity to act as or on behalf of.
|
16
|
+
#
|
17
|
+
# If not explicitly provided, the server defaults to using the identity
|
18
|
+
# that was authenticated by the external credentials.
|
19
|
+
attr_reader :authzid
|
20
|
+
|
21
|
+
# :call-seq:
|
22
|
+
# new(authzid: nil, **) -> authenticator
|
23
|
+
#
|
24
|
+
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
|
25
|
+
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
|
26
|
+
# this, see Net::IMAP#authenticate or your client's authentication
|
27
|
+
# method.
|
28
|
+
#
|
29
|
+
# #authzid is an optional identity to act as or on behalf of.
|
30
|
+
#
|
31
|
+
# Any other keyword parameters are quietly ignored.
|
32
|
+
def initialize(authzid: nil, **)
|
33
|
+
@authzid = authzid&.to_str&.encode "UTF-8"
|
34
|
+
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
|
35
|
+
raise ArgumentError, "contains NULL"
|
36
|
+
end
|
37
|
+
@done = false
|
38
|
+
end
|
39
|
+
|
40
|
+
# :call-seq:
|
41
|
+
# initial_response? -> true
|
42
|
+
#
|
43
|
+
# +EXTERNAL+ can send an initial client response.
|
44
|
+
def initial_response?; true end
|
45
|
+
|
46
|
+
# Returns #authzid, or an empty string if there is no authzid.
|
47
|
+
def process(_)
|
48
|
+
authzid || ""
|
49
|
+
ensure
|
50
|
+
@done = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns true when the initial client response was sent.
|
54
|
+
#
|
55
|
+
# The authentication should not succeed unless this returns true, but it
|
56
|
+
# does *not* indicate success.
|
57
|
+
def done?; @done end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
module SASL
|
6
|
+
|
7
|
+
# Originally defined for the GS2 mechanism family in
|
8
|
+
# RFC5801[https://tools.ietf.org/html/rfc5801],
|
9
|
+
# several different mechanisms start with a GS2 header:
|
10
|
+
# * +GS2-*+ --- RFC5801[https://tools.ietf.org/html/rfc5801]
|
11
|
+
# * +SCRAM-*+ --- RFC5802[https://tools.ietf.org/html/rfc5802]
|
12
|
+
# (ScramAuthenticator)
|
13
|
+
# * +SAML20+ --- RFC6595[https://tools.ietf.org/html/rfc6595]
|
14
|
+
# * +OPENID20+ --- RFC6616[https://tools.ietf.org/html/rfc6616]
|
15
|
+
# * +OAUTH10A+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
|
16
|
+
# * +OAUTHBEARER+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
|
17
|
+
# (OAuthBearerAuthenticator)
|
18
|
+
#
|
19
|
+
# Classes that include this module must implement +#authzid+.
|
20
|
+
module GS2Header
|
21
|
+
NO_NULL_CHARS = /\A[^\x00]+\z/u.freeze # :nodoc:
|
22
|
+
|
23
|
+
##
|
24
|
+
# Matches {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
25
|
+
# +saslname+. The output from gs2_saslname_encode matches this Regexp.
|
26
|
+
RFC5801_SASLNAME = /\A(?:[^,=\x00]|=2C|=3D)+\z/u.freeze
|
27
|
+
|
28
|
+
# The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
29
|
+
# +gs2-header+, which prefixes the #initial_client_response.
|
30
|
+
#
|
31
|
+
# >>>
|
32
|
+
# <em>Note: the actual GS2 header includes an optional flag to
|
33
|
+
# indicate that the GSS mechanism is not "standard", but since all of
|
34
|
+
# the SASL mechanisms using GS2 are "standard", we don't include that
|
35
|
+
# flag. A class for a nonstandard GSSAPI mechanism should prefix with
|
36
|
+
# "+F,+".</em>
|
37
|
+
def gs2_header
|
38
|
+
"#{gs2_cb_flag},#{gs2_authzid},"
|
39
|
+
end
|
40
|
+
|
41
|
+
# The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
42
|
+
# +gs2-cb-flag+:
|
43
|
+
#
|
44
|
+
# "+n+":: The client doesn't support channel binding.
|
45
|
+
# "+y+":: The client does support channel binding
|
46
|
+
# but thinks the server does not.
|
47
|
+
# "+p+":: The client requires channel binding.
|
48
|
+
# The selected channel binding follows "+p=+".
|
49
|
+
#
|
50
|
+
# The default always returns "+n+". A mechanism that supports channel
|
51
|
+
# binding must override this method.
|
52
|
+
#
|
53
|
+
def gs2_cb_flag; "n" end
|
54
|
+
|
55
|
+
# The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
56
|
+
# +gs2-authzid+ header, when +#authzid+ is not empty.
|
57
|
+
#
|
58
|
+
# If +#authzid+ is empty or +nil+, an empty string is returned.
|
59
|
+
def gs2_authzid
|
60
|
+
return "" if authzid.nil? || authzid == ""
|
61
|
+
"a=#{gs2_saslname_encode(authzid)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
module_function
|
65
|
+
|
66
|
+
# Encodes +str+ to match RFC5801_SASLNAME.
|
67
|
+
def gs2_saslname_encode(str)
|
68
|
+
str = str.encode("UTF-8")
|
69
|
+
# Regexp#match raises "invalid byte sequence" for invalid UTF-8
|
70
|
+
NO_NULL_CHARS.match str or
|
71
|
+
raise ArgumentError, "invalid saslname: %p" % [str]
|
72
|
+
str
|
73
|
+
.gsub(?=, "=3D")
|
74
|
+
.gsub(?,, "=2C")
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -17,21 +17,11 @@
|
|
17
17
|
# compatibility with existing servers. See
|
18
18
|
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
|
19
19
|
# for both specification and deprecation.
|
20
|
-
class Net::IMAP::LoginAuthenticator
|
21
|
-
def process(data)
|
22
|
-
case @state
|
23
|
-
when STATE_USER
|
24
|
-
@state = STATE_PASSWORD
|
25
|
-
return @user
|
26
|
-
when STATE_PASSWORD
|
27
|
-
return @password
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
20
|
+
class Net::IMAP::SASL::LoginAuthenticator
|
33
21
|
STATE_USER = :USER
|
34
22
|
STATE_PASSWORD = :PASSWORD
|
23
|
+
STATE_DONE = :DONE
|
24
|
+
private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
|
35
25
|
|
36
26
|
def initialize(user, password, warn_deprecation: true, **_ignored)
|
37
27
|
if warn_deprecation
|
@@ -42,5 +32,20 @@ class Net::IMAP::LoginAuthenticator
|
|
42
32
|
@state = STATE_USER
|
43
33
|
end
|
44
34
|
|
45
|
-
|
35
|
+
def initial_response?; false end
|
36
|
+
|
37
|
+
def process(data)
|
38
|
+
case @state
|
39
|
+
when STATE_USER
|
40
|
+
@state = STATE_PASSWORD
|
41
|
+
return @user
|
42
|
+
when STATE_PASSWORD
|
43
|
+
@state = STATE_DONE
|
44
|
+
return @password
|
45
|
+
when STATE_DONE
|
46
|
+
raise ResponseParseError, data
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def done?; @state == STATE_DONE end
|
46
51
|
end
|