net-imap 0.3.9 → 0.4.0
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.
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/errors.rb +0 -34
- 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 +987 -690
- data/net-imap.gemspec +1 -1
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +82 -60
- metadata +30 -13
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/response_reader.rb +0 -75
- 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
|