net-imap 0.4.19 → 0.5.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/Gemfile +7 -1
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +11 -0
- data/lib/net/imap/config.rb +36 -79
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/deprecated_client_options.rb +6 -3
- data/lib/net/imap/errors.rb +6 -0
- data/lib/net/imap/response_data.rb +60 -96
- data/lib/net/imap/response_parser.rb +18 -45
- data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
- data/lib/net/imap/sasl/authenticators.rb +8 -4
- data/lib/net/imap/sasl/client_adapter.rb +77 -26
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +1 -1
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +213 -51
- data/lib/net/imap/sasl/login_authenticator.rb +2 -1
- data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl.rb +6 -3
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/sequence_set.rb +70 -213
- data/lib/net/imap.rb +29 -55
- data/net-imap.gemspec +1 -1
- metadata +7 -5
- data/lib/net/imap/uidplus_data.rb +0 -326
@@ -4,44 +4,76 @@ module Net
|
|
4
4
|
class IMAP
|
5
5
|
module SASL
|
6
6
|
|
7
|
-
#
|
7
|
+
# AuthenticationExchange is used internally by Net::IMAP#authenticate.
|
8
|
+
# But the API is still *experimental*, and may change.
|
8
9
|
#
|
9
10
|
# TODO: catch exceptions in #process and send #cancel_response.
|
10
11
|
# TODO: raise an error if the command succeeds after being canceled.
|
11
12
|
# TODO: use with more clients, to verify the API can accommodate them.
|
13
|
+
# TODO: pass ClientAdapter#service to SASL.authenticator
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# ).authenticate
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# private
|
15
|
+
# An AuthenticationExchange represents a single attempt to authenticate
|
16
|
+
# a SASL client to a SASL server. It is created from a client adapter, a
|
17
|
+
# mechanism name, and a mechanism authenticator. When #authenticate is
|
18
|
+
# called, it will send the appropriate authenticate command to the server,
|
19
|
+
# returning the client response on success and raising an exception on
|
20
|
+
# failure.
|
23
21
|
#
|
24
|
-
#
|
22
|
+
# In most cases, the client will not need to use
|
23
|
+
# SASL::AuthenticationExchange directly at all. Instead, use
|
24
|
+
# SASL::ClientAdapter#authenticate. If customizations are needed, the
|
25
|
+
# custom client adapter is probably the best place for that code.
|
25
26
|
#
|
26
|
-
# Or delegate creation of the authenticator to ::build:
|
27
27
|
# def authenticate(...)
|
28
|
-
#
|
29
|
-
# .authenticate
|
28
|
+
# MyClient::SASLAdapter.new(self).authenticate(...)
|
30
29
|
# end
|
31
30
|
#
|
32
|
-
#
|
31
|
+
# SASL::ClientAdapter#authenticate delegates to ::authenticate, like so:
|
32
|
+
#
|
33
33
|
# def authenticate(...)
|
34
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
34
35
|
# SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
|
35
36
|
# end
|
36
37
|
#
|
37
|
-
#
|
38
|
-
#
|
38
|
+
# ::authenticate simply delegates to ::build and #authenticate, like so:
|
39
|
+
#
|
40
|
+
# def authenticate(...)
|
41
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
42
|
+
# SASL::AuthenticationExchange
|
43
|
+
# .build(sasl_adapter, ...)
|
44
|
+
# .authenticate
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# And ::build delegates to SASL.authenticator and ::new, like so:
|
48
|
+
#
|
49
|
+
# def authenticate(mechanism, ...)
|
50
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
51
|
+
# authenticator = SASL.authenticator(mechanism, ...)
|
52
|
+
# SASL::AuthenticationExchange
|
53
|
+
# .new(sasl_adapter, mechanism, authenticator)
|
54
|
+
# .authenticate
|
55
|
+
# end
|
39
56
|
#
|
40
57
|
class AuthenticationExchange
|
41
58
|
# Convenience method for <tt>build(...).authenticate</tt>
|
59
|
+
#
|
60
|
+
# See also: SASL::ClientAdapter#authenticate
|
42
61
|
def self.authenticate(...) build(...).authenticate end
|
43
62
|
|
44
|
-
#
|
63
|
+
# Convenience method to combine the creation of a new authenticator and
|
64
|
+
# a new Authentication exchange.
|
65
|
+
#
|
66
|
+
# +client+ must be an instance of SASL::ClientAdapter.
|
67
|
+
#
|
68
|
+
# +mechanism+ must be a SASL mechanism name, as a string or symbol.
|
69
|
+
#
|
70
|
+
# +sasl_ir+ allows or disallows sending an "initial response", depending
|
71
|
+
# also on whether the server capabilities, mechanism authenticator, and
|
72
|
+
# client adapter all support it. Defaults to +true+.
|
73
|
+
#
|
74
|
+
# +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to
|
75
|
+
# SASL.authenticator. Use the +registry+ kwarg to override the global
|
76
|
+
# SASL::Authenticators registry.
|
45
77
|
def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
|
46
78
|
authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
|
47
79
|
new(client, mechanism, authenticator, sasl_ir: sasl_ir)
|
@@ -51,7 +83,7 @@ module Net
|
|
51
83
|
|
52
84
|
def initialize(client, mechanism, authenticator, sasl_ir: true)
|
53
85
|
@client = client
|
54
|
-
@mechanism =
|
86
|
+
@mechanism = Authenticators.normalize_name(mechanism)
|
55
87
|
@authenticator = authenticator
|
56
88
|
@sasl_ir = sasl_ir
|
57
89
|
@processed = false
|
@@ -21,6 +21,10 @@ module Net::IMAP::SASL
|
|
21
21
|
# ScramSHA1Authenticator for examples.
|
22
22
|
class Authenticators
|
23
23
|
|
24
|
+
# Normalize the mechanism name as an uppercase string, with underscores
|
25
|
+
# converted to dashes.
|
26
|
+
def self.normalize_name(mechanism) -(mechanism.to_s.upcase.tr(?_, ?-)) end
|
27
|
+
|
24
28
|
# Create a new Authenticators registry.
|
25
29
|
#
|
26
30
|
# This class is usually not instantiated directly. Use SASL.authenticators
|
@@ -65,7 +69,6 @@ module Net::IMAP::SASL
|
|
65
69
|
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
|
66
70
|
# preserved and non-alphanumeric characters are removed..
|
67
71
|
def add_authenticator(name, authenticator = nil)
|
68
|
-
key = -name.to_s.upcase.tr(?_, ?-)
|
69
72
|
authenticator ||= begin
|
70
73
|
class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
|
71
74
|
auth_class = nil
|
@@ -74,17 +77,18 @@ module Net::IMAP::SASL
|
|
74
77
|
auth_class.new(*creds, **props, &block)
|
75
78
|
}
|
76
79
|
end
|
80
|
+
key = Authenticators.normalize_name(name)
|
77
81
|
@authenticators[key] = authenticator
|
78
82
|
end
|
79
83
|
|
80
84
|
# Removes the authenticator registered for +name+
|
81
85
|
def remove_authenticator(name)
|
82
|
-
key =
|
86
|
+
key = Authenticators.normalize_name(name)
|
83
87
|
@authenticators.delete(key)
|
84
88
|
end
|
85
89
|
|
86
90
|
def mechanism?(name)
|
87
|
-
key =
|
91
|
+
key = Authenticators.normalize_name(name)
|
88
92
|
@authenticators.key?(key)
|
89
93
|
end
|
90
94
|
|
@@ -105,7 +109,7 @@ module Net::IMAP::SASL
|
|
105
109
|
# only. Protocol client users should see refer to their client's
|
106
110
|
# documentation, e.g. Net::IMAP#authenticate.
|
107
111
|
def authenticator(mechanism, ...)
|
108
|
-
key =
|
112
|
+
key = Authenticators.normalize_name(mechanism)
|
109
113
|
auth = @authenticators.fetch(key) do
|
110
114
|
raise ArgumentError, 'unknown auth type - "%s"' % key
|
111
115
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "forwardable"
|
4
|
+
|
3
5
|
module Net
|
4
6
|
class IMAP
|
5
7
|
module SASL
|
@@ -8,42 +10,76 @@ module Net
|
|
8
10
|
#
|
9
11
|
# TODO: use with more clients, to verify the API can accommodate them.
|
10
12
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# to match
|
13
|
+
# Represents the client to a SASL::AuthenticationExchange. By default,
|
14
|
+
# most methods simply delegate to #client. Clients should subclass
|
15
|
+
# SASL::ClientAdapter and override methods as needed to match the
|
16
|
+
# semantics of this API to their API.
|
14
17
|
#
|
15
|
-
#
|
16
|
-
# will probably need to override some methods. Additionally, subclasses
|
17
|
-
# may need to include a protocol adapter mixin, if the default
|
18
|
+
# Subclasses should also include a protocol adapter mixin when the default
|
18
19
|
# ProtocolAdapters::Generic isn't sufficient.
|
20
|
+
#
|
21
|
+
# === Protocol Requirements
|
22
|
+
#
|
23
|
+
# {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4]
|
24
|
+
# lists requirements for protocol specifications to offer SASL. Where
|
25
|
+
# possible, ClientAdapter delegates the handling of these requirements to
|
26
|
+
# SASL::ProtocolAdapters.
|
19
27
|
class ClientAdapter
|
28
|
+
extend Forwardable
|
29
|
+
|
20
30
|
include ProtocolAdapters::Generic
|
21
31
|
|
22
|
-
|
32
|
+
# The client that handles communication with the protocol server.
|
33
|
+
#
|
34
|
+
# Most ClientAdapter methods are simply delegated to #client by default.
|
35
|
+
attr_reader :client
|
23
36
|
|
24
37
|
# +command_proc+ can used to avoid exposing private methods on #client.
|
25
|
-
# It
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
38
|
+
# It's value is set by the block that is passed to ::new, and it is used
|
39
|
+
# by the default implementation of #run_command. Subclasses that
|
40
|
+
# override #run_command may use #command_proc for any other purpose they
|
41
|
+
# find useful.
|
29
42
|
#
|
30
|
-
#
|
31
|
-
#
|
43
|
+
# In the default implementation of #run_command, command_proc is called
|
44
|
+
# with the protocols authenticate +command+ name, the +mechanism+ name,
|
45
|
+
# an _optional_ +initial_response+ argument, and a +continuations+
|
46
|
+
# block. command_proc must run the protocol command with the arguments
|
47
|
+
# sent to it, _yield_ the payload of each continuation, respond to the
|
48
|
+
# continuation with the result of each _yield_, and _return_ the
|
49
|
+
# command's successful result. Non-successful results *MUST* raise
|
50
|
+
# an exception.
|
51
|
+
attr_reader :command_proc
|
52
|
+
|
53
|
+
# By default, this simply sets the #client and #command_proc attributes.
|
54
|
+
# Subclasses may override it, for example: to set the appropriate
|
55
|
+
# command_proc automatically.
|
32
56
|
def initialize(client, &command_proc)
|
33
57
|
@client, @command_proc = client, command_proc
|
34
58
|
end
|
35
59
|
|
36
|
-
#
|
60
|
+
# Attempt to authenticate #client to the server.
|
61
|
+
#
|
62
|
+
# By default, this simply delegates to
|
63
|
+
# AuthenticationExchange.authenticate.
|
37
64
|
def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
|
38
65
|
|
39
|
-
|
40
|
-
|
66
|
+
##
|
67
|
+
# method: sasl_ir_capable?
|
68
|
+
# Do the protocol, server, and client all support an initial response?
|
69
|
+
def_delegator :client, :sasl_ir_capable?
|
41
70
|
|
42
|
-
|
43
|
-
|
71
|
+
##
|
72
|
+
# method: auth_capable?
|
73
|
+
# call-seq: auth_capable?(mechanism)
|
74
|
+
#
|
75
|
+
# Does the server advertise support for the +mechanism+?
|
76
|
+
def_delegator :client, :auth_capable?
|
44
77
|
|
45
|
-
#
|
46
|
-
#
|
78
|
+
# Calls command_proc with +command_name+ (see
|
79
|
+
# SASL::ProtocolAdapters::Generic#command_name),
|
80
|
+
# +mechanism+, +initial_response+, and a +continuations_handler+ block.
|
81
|
+
# The +initial_response+ is optional; when it's nil, it won't be sent to
|
82
|
+
# command_proc.
|
47
83
|
#
|
48
84
|
# Yields each continuation payload, responds to the server with the
|
49
85
|
# result of each yield, and returns the result. Non-successful results
|
@@ -51,21 +87,36 @@ module Net
|
|
51
87
|
# command to fail.
|
52
88
|
#
|
53
89
|
# Subclasses that override this may use #command_proc differently.
|
54
|
-
def run_command(mechanism, initial_response = nil, &
|
90
|
+
def run_command(mechanism, initial_response = nil, &continuations_handler)
|
55
91
|
command_proc or raise Error, "initialize with block or override"
|
56
92
|
args = [command_name, mechanism, initial_response].compact
|
57
|
-
command_proc.call(*args, &
|
93
|
+
command_proc.call(*args, &continuations_handler)
|
58
94
|
end
|
59
95
|
|
96
|
+
##
|
97
|
+
# method: host
|
98
|
+
# The hostname to which the client connected.
|
99
|
+
def_delegator :client, :host
|
100
|
+
|
101
|
+
##
|
102
|
+
# method: port
|
103
|
+
# The destination port to which the client connected.
|
104
|
+
def_delegator :client, :port
|
105
|
+
|
60
106
|
# Returns an array of server responses errors raised by run_command.
|
61
107
|
# Exceptions in this array won't drop the connection.
|
62
108
|
def response_errors; [] end
|
63
109
|
|
64
|
-
|
65
|
-
|
110
|
+
##
|
111
|
+
# method: drop_connection
|
112
|
+
# Drop the connection gracefully, sending a "LOGOUT" command as needed.
|
113
|
+
def_delegator :client, :drop_connection
|
114
|
+
|
115
|
+
##
|
116
|
+
# method: drop_connection!
|
117
|
+
# Drop the connection abruptly, closing the socket without logging out.
|
118
|
+
def_delegator :client, :drop_connection!
|
66
119
|
|
67
|
-
# Drop the connection abruptly.
|
68
|
-
def drop_connection!; client.drop_connection! end
|
69
120
|
end
|
70
121
|
end
|
71
122
|
end
|
@@ -20,7 +20,7 @@ class Net::IMAP::SASL::CramMD5Authenticator
|
|
20
20
|
warn_deprecation: true,
|
21
21
|
**)
|
22
22
|
if warn_deprecation
|
23
|
-
warn "WARNING: CRAM-MD5 mechanism is deprecated."
|
23
|
+
warn "WARNING: CRAM-MD5 mechanism is deprecated.", category: :deprecated
|
24
24
|
end
|
25
25
|
require "digest/md5"
|
26
26
|
@user = authcid || username || user
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Net::IMAP authenticator for the
|
3
|
+
# Net::IMAP authenticator for the +DIGEST-MD5+ SASL mechanism type, specified
|
4
4
|
# in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
|
5
5
|
#
|
6
6
|
# == Deprecated
|
@@ -9,11 +9,32 @@
|
|
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
11
|
class Net::IMAP::SASL::DigestMD5Authenticator
|
12
|
+
DataFormatError = Net::IMAP::DataFormatError
|
13
|
+
ResponseParseError = Net::IMAP::ResponseParseError
|
14
|
+
private_constant :DataFormatError, :ResponseParseError
|
15
|
+
|
12
16
|
STAGE_ONE = :stage_one
|
13
17
|
STAGE_TWO = :stage_two
|
14
18
|
STAGE_DONE = :stage_done
|
15
19
|
private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
|
16
20
|
|
21
|
+
# Directives which must not have multiples. The RFC states:
|
22
|
+
# >>>
|
23
|
+
# This directive may appear at most once; if multiple instances are present,
|
24
|
+
# the client should abort the authentication exchange.
|
25
|
+
NO_MULTIPLES = %w[nonce stale maxbuf charset algorithm].freeze
|
26
|
+
|
27
|
+
# Required directives which must occur exactly once. The RFC states: >>>
|
28
|
+
# This directive is required and MUST appear exactly once; if not present,
|
29
|
+
# or if multiple instances are present, the client should abort the
|
30
|
+
# authentication exchange.
|
31
|
+
REQUIRED = %w[nonce algorithm].freeze
|
32
|
+
|
33
|
+
# Directives which are composed of one or more comma delimited tokens
|
34
|
+
QUOTED_LISTABLE = %w[qop cipher].freeze
|
35
|
+
|
36
|
+
private_constant :NO_MULTIPLES, :REQUIRED, :QUOTED_LISTABLE
|
37
|
+
|
17
38
|
# Authentication identity: the identity that matches the #password.
|
18
39
|
#
|
19
40
|
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
|
@@ -42,6 +63,59 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
42
63
|
#
|
43
64
|
attr_reader :authzid
|
44
65
|
|
66
|
+
# A namespace or collection of identities which contains +username+.
|
67
|
+
#
|
68
|
+
# Used by DIGEST-MD5, GSS-API, and NTLM. This is often a domain name that
|
69
|
+
# contains the name of the host performing the authentication.
|
70
|
+
#
|
71
|
+
# <em>Defaults to the last realm in the server-provided list of
|
72
|
+
# realms.</em>
|
73
|
+
attr_reader :realm
|
74
|
+
|
75
|
+
# Fully qualified canonical DNS host name for the requested service.
|
76
|
+
#
|
77
|
+
# <em>Defaults to #realm.</em>
|
78
|
+
attr_reader :host
|
79
|
+
|
80
|
+
# The service protocol, a
|
81
|
+
# {registered GSSAPI service name}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml],
|
82
|
+
# e.g. "imap", "ldap", or "xmpp".
|
83
|
+
#
|
84
|
+
# For Net::IMAP, the default is "imap" and should not be overridden. This
|
85
|
+
# must be set appropriately to use authenticators in other protocols.
|
86
|
+
#
|
87
|
+
# If an IANA-registered name isn't available, GSS-API
|
88
|
+
# (RFC-2743[https://tools.ietf.org/html/rfc2743]) allows the generic name
|
89
|
+
# "host".
|
90
|
+
attr_reader :service
|
91
|
+
|
92
|
+
# The generic server name when the server is replicated.
|
93
|
+
#
|
94
|
+
# +service_name+ will be ignored when it is +nil+ or identical to +host+.
|
95
|
+
#
|
96
|
+
# From RFC-2831[https://tools.ietf.org/html/rfc2831]:
|
97
|
+
# >>>
|
98
|
+
# The service is considered to be replicated if the client's
|
99
|
+
# service-location process involves resolution using standard DNS lookup
|
100
|
+
# operations, and if these operations involve DNS records (such as SRV, or
|
101
|
+
# MX) which resolve one DNS name into a set of other DNS names. In this
|
102
|
+
# case, the initial name used by the client is the "serv-name", and the
|
103
|
+
# final name is the "host" component.
|
104
|
+
attr_reader :service_name
|
105
|
+
|
106
|
+
# Parameters sent by the server are stored in this hash.
|
107
|
+
attr_reader :sparams
|
108
|
+
|
109
|
+
# The charset sent by the server. "UTF-8" (case insensitive) is the only
|
110
|
+
# allowed value. +nil+ should be interpreted as ISO 8859-1.
|
111
|
+
attr_reader :charset
|
112
|
+
|
113
|
+
# nonce sent by the server
|
114
|
+
attr_reader :nonce
|
115
|
+
|
116
|
+
# qop-options sent by the server
|
117
|
+
attr_reader :qop
|
118
|
+
|
45
119
|
# :call-seq:
|
46
120
|
# new(username, password, authzid = nil, **options) -> authenticator
|
47
121
|
# new(username:, password:, authzid: nil, **options) -> authenticator
|
@@ -64,27 +138,59 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
64
138
|
# When +authzid+ is not set, the server should derive the authorization
|
65
139
|
# identity from the authentication identity.
|
66
140
|
#
|
141
|
+
# * _optional_ #realm — A namespace for the #username, e.g. a domain.
|
142
|
+
# <em>Defaults to the last realm in the server-provided realms list.</em>
|
143
|
+
# * _optional_ #host — FQDN for requested service.
|
144
|
+
# <em>Defaults to</em> #realm.
|
145
|
+
# * _optional_ #service_name — The generic host name when the server is
|
146
|
+
# replicated.
|
147
|
+
# * _optional_ #service — the registered service protocol. E.g. "imap",
|
148
|
+
# "smtp", "ldap", "xmpp".
|
149
|
+
# <em>For Net::IMAP, this defaults to "imap".</em>
|
150
|
+
#
|
67
151
|
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
|
68
152
|
#
|
69
153
|
# Any other keyword arguments are silently ignored.
|
70
154
|
def initialize(user = nil, pass = nil, authz = nil,
|
71
155
|
username: nil, password: nil, authzid: nil,
|
72
156
|
authcid: nil, secret: nil,
|
157
|
+
realm: nil, service: "imap", host: nil, service_name: nil,
|
73
158
|
warn_deprecation: true, **)
|
74
159
|
username = authcid || username || user or
|
75
160
|
raise ArgumentError, "missing username (authcid)"
|
76
161
|
password ||= secret || pass or raise ArgumentError, "missing password"
|
77
162
|
authzid ||= authz
|
78
163
|
if warn_deprecation
|
79
|
-
warn
|
80
|
-
|
164
|
+
warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
|
165
|
+
category: :deprecated)
|
81
166
|
end
|
167
|
+
|
82
168
|
require "digest/md5"
|
169
|
+
require "securerandom"
|
83
170
|
require "strscan"
|
84
171
|
@username, @password, @authzid = username, password, authzid
|
172
|
+
@realm = realm
|
173
|
+
@host = host
|
174
|
+
@service = service
|
175
|
+
@service_name = service_name
|
85
176
|
@nc, @stage = {}, STAGE_ONE
|
86
177
|
end
|
87
178
|
|
179
|
+
# From RFC-2831[https://tools.ietf.org/html/rfc2831]:
|
180
|
+
# >>>
|
181
|
+
# Indicates the principal name of the service with which the client wishes
|
182
|
+
# to connect, formed from the serv-type, host, and serv-name. For
|
183
|
+
# example, the FTP service on "ftp.example.com" would have a "digest-uri"
|
184
|
+
# value of "ftp/ftp.example.com"; the SMTP server from the example above
|
185
|
+
# would have a "digest-uri" value of "smtp/mail3.example.com/example.com".
|
186
|
+
def digest_uri
|
187
|
+
if service_name && service_name != host
|
188
|
+
"#{service}/#{host}/#{service_name}"
|
189
|
+
else
|
190
|
+
"#{service}/#{host}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
88
194
|
def initial_response?; false end
|
89
195
|
|
90
196
|
# Responds to server challenge in two stages.
|
@@ -92,78 +198,134 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
92
198
|
case @stage
|
93
199
|
when STAGE_ONE
|
94
200
|
@stage = STAGE_TWO
|
95
|
-
sparams =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if v =~ /,/
|
102
|
-
v = v.split(',')
|
103
|
-
end
|
104
|
-
end
|
105
|
-
sparams[k] = v
|
106
|
-
end
|
201
|
+
@sparams = parse_challenge(challenge)
|
202
|
+
@qop = sparams.key?("qop") ? ["auth"] : sparams["qop"].flatten
|
203
|
+
@nonce = sparams["nonce"] &.first
|
204
|
+
@charset = sparams["charset"]&.first
|
205
|
+
@realm ||= sparams["realm"] &.last
|
206
|
+
@host ||= realm
|
107
207
|
|
108
|
-
|
109
|
-
|
208
|
+
if !qop.include?("auth")
|
209
|
+
raise DataFormatError, "Server does not support auth (qop = %p)" % [
|
210
|
+
sparams["qop"]
|
211
|
+
]
|
212
|
+
elsif (emptykey = REQUIRED.find { sparams[_1].empty? })
|
213
|
+
raise DataFormatError, "Server didn't send %s (%p)" % [emptykey, challenge]
|
214
|
+
elsif (multikey = NO_MULTIPLES.find { sparams[_1].length > 1 })
|
215
|
+
raise DataFormatError, "Server sent multiple %s (%p)" % [multikey, challenge]
|
216
|
+
end
|
110
217
|
|
111
218
|
response = {
|
112
|
-
:nonce
|
113
|
-
:username
|
114
|
-
:realm
|
115
|
-
:
|
116
|
-
|
117
|
-
:
|
118
|
-
:
|
119
|
-
:
|
120
|
-
:charset
|
219
|
+
nonce: nonce,
|
220
|
+
username: username,
|
221
|
+
realm: realm,
|
222
|
+
cnonce: SecureRandom.base64(32),
|
223
|
+
"digest-uri": digest_uri,
|
224
|
+
qop: "auth",
|
225
|
+
maxbuf: 65535,
|
226
|
+
nc: "%08d" % nc(nonce),
|
227
|
+
charset: charset,
|
121
228
|
}
|
122
229
|
|
123
230
|
response[:authzid] = @authzid unless @authzid.nil?
|
124
231
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
|
129
|
-
a1 << ':' + response[:authzid] unless response[:authzid].nil?
|
130
|
-
|
131
|
-
a2 = "AUTHENTICATE:" + response[:'digest-uri']
|
132
|
-
a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
133
|
-
|
134
|
-
response[:response] = Digest::MD5.hexdigest(
|
135
|
-
[
|
136
|
-
Digest::MD5.hexdigest(a1),
|
137
|
-
response.values_at(:nonce, :nc, :cnonce, :qop),
|
138
|
-
Digest::MD5.hexdigest(a2)
|
139
|
-
].join(':')
|
140
|
-
)
|
141
|
-
|
142
|
-
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
|
232
|
+
response[:response] = response_value(response)
|
233
|
+
format_response(response)
|
143
234
|
when STAGE_TWO
|
144
235
|
@stage = STAGE_DONE
|
145
|
-
|
146
|
-
if
|
147
|
-
return ''
|
148
|
-
else
|
149
|
-
raise ResponseParseError, challenge
|
150
|
-
end
|
236
|
+
raise ResponseParseError, challenge unless challenge =~ /rspauth=/
|
237
|
+
"" # if at the second stage, return an empty string
|
151
238
|
else
|
152
239
|
raise ResponseParseError, challenge
|
153
240
|
end
|
241
|
+
rescue => error
|
242
|
+
@stage = error
|
243
|
+
raise
|
154
244
|
end
|
155
245
|
|
156
246
|
def done?; @stage == STAGE_DONE end
|
157
247
|
|
158
248
|
private
|
159
249
|
|
250
|
+
LWS = /[\r\n \t]*/n # less strict than RFC, more strict than '\s'
|
251
|
+
TOKEN = /[^\x00-\x20\x7f()<>@,;:\\"\/\[\]?={}]+/n
|
252
|
+
QUOTED_STR = /"(?: [\t\x20-\x7e&&[^"]] | \\[\x00-\x7f] )*"/nx
|
253
|
+
LIST_DELIM = /(?:#{LWS} , )+ #{LWS}/nx
|
254
|
+
AUTH_PARAM = /
|
255
|
+
(#{TOKEN}) #{LWS} = #{LWS} (#{QUOTED_STR} | #{TOKEN}) #{LIST_DELIM}?
|
256
|
+
/nx
|
257
|
+
private_constant :LWS, :TOKEN, :QUOTED_STR, :LIST_DELIM, :AUTH_PARAM
|
258
|
+
|
259
|
+
def parse_challenge(challenge)
|
260
|
+
sparams = Hash.new {|h, k| h[k] = [] }
|
261
|
+
c = StringScanner.new(challenge)
|
262
|
+
c.skip LIST_DELIM
|
263
|
+
while c.scan AUTH_PARAM
|
264
|
+
k, v = c[1], c[2]
|
265
|
+
k = k.downcase
|
266
|
+
if v =~ /\A"(.*)"\z/mn
|
267
|
+
v = $1.gsub(/\\(.)/mn, '\1')
|
268
|
+
v = split_quoted_list(v, challenge) if QUOTED_LISTABLE.include? k
|
269
|
+
end
|
270
|
+
sparams[k] << v
|
271
|
+
end
|
272
|
+
if !c.eos?
|
273
|
+
raise DataFormatError, "Unparsable challenge: %p" % [challenge]
|
274
|
+
elsif sparams.empty?
|
275
|
+
raise DataFormatError, "Empty challenge: %p" % [challenge]
|
276
|
+
end
|
277
|
+
sparams
|
278
|
+
end
|
279
|
+
|
280
|
+
def split_quoted_list(value, challenge)
|
281
|
+
value.split(LIST_DELIM).reject(&:empty?).tap do
|
282
|
+
_1.any? or raise DataFormatError, "Bad Challenge: %p" % [challenge]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
160
286
|
def nc(nonce)
|
161
287
|
if @nc.has_key? nonce
|
162
288
|
@nc[nonce] = @nc[nonce] + 1
|
163
289
|
else
|
164
290
|
@nc[nonce] = 1
|
165
291
|
end
|
166
|
-
|
292
|
+
end
|
293
|
+
|
294
|
+
def response_value(response)
|
295
|
+
a1 = compute_a1(response)
|
296
|
+
a2 = compute_a2(response)
|
297
|
+
Digest::MD5.hexdigest(
|
298
|
+
[
|
299
|
+
Digest::MD5.hexdigest(a1),
|
300
|
+
response.values_at(:nonce, :nc, :cnonce, :qop),
|
301
|
+
Digest::MD5.hexdigest(a2)
|
302
|
+
].join(":")
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
def compute_a0(response)
|
307
|
+
Digest::MD5.digest(
|
308
|
+
[ response.values_at(:username, :realm), password ].join(":")
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
def compute_a1(response)
|
313
|
+
a0 = compute_a0(response)
|
314
|
+
a1 = [ a0, response.values_at(:nonce, :cnonce) ].join(":")
|
315
|
+
a1 << ":#{response[:authzid]}" unless response[:authzid].nil?
|
316
|
+
a1
|
317
|
+
end
|
318
|
+
|
319
|
+
def compute_a2(response)
|
320
|
+
a2 = "AUTHENTICATE:#{response[:"digest-uri"]}"
|
321
|
+
if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
322
|
+
a2 << ":00000000000000000000000000000000"
|
323
|
+
end
|
324
|
+
a2
|
325
|
+
end
|
326
|
+
|
327
|
+
def format_response(response)
|
328
|
+
response.map {|k, v| qdval(k.to_s, v) }.join(",")
|
167
329
|
end
|
168
330
|
|
169
331
|
# some responses need quoting
|
@@ -29,7 +29,8 @@ class Net::IMAP::SASL::LoginAuthenticator
|
|
29
29
|
warn_deprecation: true,
|
30
30
|
**)
|
31
31
|
if warn_deprecation
|
32
|
-
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
|
32
|
+
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.",
|
33
|
+
category: :deprecated
|
33
34
|
end
|
34
35
|
@user = authcid || username || user
|
35
36
|
@password = password || secret || pass
|