net-imap 0.3.4 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/BSDL +22 -0
  3. data/COPYING +56 -0
  4. data/Gemfile +14 -0
  5. data/LICENSE.txt +3 -22
  6. data/README.md +25 -8
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +72 -23
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +74 -54
  11. data/lib/net/imap/config/attr_accessors.rb +75 -0
  12. data/lib/net/imap/config/attr_inheritance.rb +90 -0
  13. data/lib/net/imap/config/attr_type_coercion.rb +61 -0
  14. data/lib/net/imap/config.rb +470 -0
  15. data/lib/net/imap/data_encoding.rb +21 -9
  16. data/lib/net/imap/data_lite.rb +226 -0
  17. data/lib/net/imap/deprecated_client_options.rb +142 -0
  18. data/lib/net/imap/errors.rb +27 -1
  19. data/lib/net/imap/esearch_result.rb +180 -0
  20. data/lib/net/imap/fetch_data.rb +597 -0
  21. data/lib/net/imap/flags.rb +1 -1
  22. data/lib/net/imap/response_data.rb +250 -440
  23. data/lib/net/imap/response_parser/parser_utils.rb +245 -0
  24. data/lib/net/imap/response_parser.rb +1867 -1184
  25. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  26. data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
  27. data/lib/net/imap/sasl/authenticators.rb +122 -0
  28. data/lib/net/imap/sasl/client_adapter.rb +123 -0
  29. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
  30. data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
  31. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  32. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  33. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
  34. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  35. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  36. data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
  37. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  38. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  39. data/lib/net/imap/sasl/stringprep.rb +6 -66
  40. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  41. data/lib/net/imap/sasl.rb +148 -44
  42. data/lib/net/imap/sasl_adapter.rb +20 -0
  43. data/lib/net/imap/search_result.rb +146 -0
  44. data/lib/net/imap/sequence_set.rb +1565 -0
  45. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  46. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  47. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  48. data/lib/net/imap/stringprep/tables.rb +146 -0
  49. data/lib/net/imap/stringprep/trace.rb +85 -0
  50. data/lib/net/imap/stringprep.rb +159 -0
  51. data/lib/net/imap/uidplus_data.rb +244 -0
  52. data/lib/net/imap/vanished_data.rb +56 -0
  53. data/lib/net/imap.rb +2090 -823
  54. data/net-imap.gemspec +7 -8
  55. data/rakelib/benchmarks.rake +91 -0
  56. data/rakelib/rfcs.rake +2 -0
  57. data/rakelib/saslprep.rake +4 -4
  58. data/rakelib/string_prep_tables_generator.rb +84 -60
  59. data/sample/net-imap.rb +167 -0
  60. metadata +45 -49
  61. data/.github/dependabot.yml +0 -6
  62. data/.github/workflows/test.yml +0 -31
  63. data/.gitignore +0 -10
  64. data/benchmarks/stringprep.yml +0 -65
  65. data/benchmarks/table-regexps.yml +0 -39
  66. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  67. data/lib/net/imap/authenticators/plain.rb +0 -41
  68. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  69. data/lib/net/imap/sasl/saslprep.rb +0 -55
  70. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  71. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Net::IMAP authenticator for the +DIGEST-MD5+ SASL mechanism type, specified
4
+ # in RFC-2831[https://www.rfc-editor.org/rfc/rfc2831]. See Net::IMAP#authenticate.
5
+ #
6
+ # == Deprecated
7
+ #
8
+ # "+DIGEST-MD5+" has been deprecated by
9
+ # RFC-6331[https://www.rfc-editor.org/rfc/rfc6331] and should not be relied on for
10
+ # security. It is included for compatibility with existing servers.
11
+ class Net::IMAP::SASL::DigestMD5Authenticator
12
+ DataFormatError = Net::IMAP::DataFormatError
13
+ ResponseParseError = Net::IMAP::ResponseParseError
14
+ private_constant :DataFormatError, :ResponseParseError
15
+
16
+ STAGE_ONE = :stage_one
17
+ STAGE_TWO = :stage_two
18
+ STAGE_DONE = :stage_done
19
+ private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
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
+
38
+ # Authentication identity: the identity that matches the #password.
39
+ #
40
+ # RFC-2831[https://www.rfc-editor.org/rfc/rfc2831] uses the term +username+.
41
+ # "Authentication identity" is the generic term used by
42
+ # RFC-4422[https://www.rfc-editor.org/rfc/rfc4422].
43
+ # RFC-4616[https://www.rfc-editor.org/rfc/rfc4616] and many later RFCs abbreviate
44
+ # this to +authcid+.
45
+ attr_reader :username
46
+ alias authcid username
47
+
48
+ # A password or passphrase that matches the #username.
49
+ #
50
+ # The +password+ will be used to create the response digest.
51
+ attr_reader :password
52
+
53
+ # Authorization identity: an identity to act as or on behalf of. The identity
54
+ # form is application protocol specific. If not provided or left blank, the
55
+ # server derives an authorization identity from the authentication identity.
56
+ # The server is responsible for verifying the client's credentials and
57
+ # verifying that the identity it associates with the client's authentication
58
+ # identity is allowed to act as (or on behalf of) the authorization identity.
59
+ #
60
+ # For example, an administrator or superuser might take on another role:
61
+ #
62
+ # imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
63
+ #
64
+ attr_reader :authzid
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://www.rfc-editor.org/rfc/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://www.rfc-editor.org/rfc/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
+
119
+ # :call-seq:
120
+ # new(username, password, authzid = nil, **options) -> authenticator
121
+ # new(username:, password:, authzid: nil, **options) -> authenticator
122
+ # new(authcid:, password:, authzid: nil, **options) -> authenticator
123
+ #
124
+ # Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
125
+ #
126
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
127
+ #
128
+ # ==== Parameters
129
+ #
130
+ # * #authcid ― Authentication identity that is associated with #password.
131
+ #
132
+ # #username ― An alias for +authcid+.
133
+ #
134
+ # * #password ― A password or passphrase associated with this #authcid.
135
+ #
136
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
137
+ #
138
+ # When +authzid+ is not set, the server should derive the authorization
139
+ # identity from the authentication identity.
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
+ #
151
+ # * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
152
+ #
153
+ # Any other keyword arguments are silently ignored.
154
+ def initialize(user = nil, pass = nil, authz = nil,
155
+ username: nil, password: nil, authzid: nil,
156
+ authcid: nil, secret: nil,
157
+ realm: nil, service: "imap", host: nil, service_name: nil,
158
+ warn_deprecation: true, **)
159
+ username = authcid || username || user or
160
+ raise ArgumentError, "missing username (authcid)"
161
+ password ||= secret || pass or raise ArgumentError, "missing password"
162
+ authzid ||= authz
163
+ if warn_deprecation
164
+ warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
165
+ category: :deprecated)
166
+ end
167
+
168
+ require "digest/md5"
169
+ require "securerandom"
170
+ require "strscan"
171
+ @username, @password, @authzid = username, password, authzid
172
+ @realm = realm
173
+ @host = host
174
+ @service = service
175
+ @service_name = service_name
176
+ @nc, @stage = {}, STAGE_ONE
177
+ end
178
+
179
+ # From RFC-2831[https://www.rfc-editor.org/rfc/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
+
194
+ def initial_response?; false end
195
+
196
+ # Responds to server challenge in two stages.
197
+ def process(challenge)
198
+ case @stage
199
+ when STAGE_ONE
200
+ @stage = STAGE_TWO
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
207
+
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
217
+
218
+ response = {
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,
228
+ }
229
+
230
+ response[:authzid] = @authzid unless @authzid.nil?
231
+
232
+ response[:response] = response_value(response)
233
+ format_response(response)
234
+ when STAGE_TWO
235
+ @stage = STAGE_DONE
236
+ raise ResponseParseError, challenge unless challenge =~ /rspauth=/
237
+ "" # if at the second stage, return an empty string
238
+ else
239
+ raise ResponseParseError, challenge
240
+ end
241
+ rescue => error
242
+ @stage = error
243
+ raise
244
+ end
245
+
246
+ def done?; @stage == STAGE_DONE end
247
+
248
+ private
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
+
286
+ def nc(nonce)
287
+ if @nc.has_key? nonce
288
+ @nc[nonce] = @nc[nonce] + 1
289
+ else
290
+ @nc[nonce] = 1
291
+ end
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(",")
329
+ end
330
+
331
+ # some responses need quoting
332
+ def qdval(k, v)
333
+ return if k.nil? or v.nil?
334
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
335
+ v = v.gsub(/([\\"])/, "\\\1")
336
+ return '%s="%s"' % [k, v]
337
+ else
338
+ return '%s=%s' % [k, v]
339
+ end
340
+ end
341
+
342
+ end
@@ -0,0 +1,83 @@
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://www.rfc-editor.org/rfc/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. The
16
+ # identity form is application protocol specific. If not provided or
17
+ # left blank, the server derives an authorization identity from the
18
+ # authentication identity. The server is responsible for verifying the
19
+ # client's credentials and verifying that the identity it associates
20
+ # with the client's authentication identity is allowed to act as (or on
21
+ # behalf of) the authorization identity.
22
+ #
23
+ # For example, an administrator or superuser might take on another role:
24
+ #
25
+ # imap.authenticate "PLAIN", "root", passwd, authzid: "user"
26
+ #
27
+ attr_reader :authzid
28
+ alias username authzid
29
+
30
+ # :call-seq:
31
+ # new(authzid: nil, **) -> authenticator
32
+ # new(username: nil, **) -> authenticator
33
+ # new(username = nil, **) -> authenticator
34
+ #
35
+ # Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
36
+ # specified in RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. To use
37
+ # this, see Net::IMAP#authenticate or your client's authentication
38
+ # method.
39
+ #
40
+ # ==== Parameters
41
+ #
42
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
43
+ #
44
+ # _optional_ #username ― An alias for #authzid.
45
+ #
46
+ # Note that, unlike some other authenticators, +username+ sets the
47
+ # _authorization_ identity and not the _authentication_ identity. The
48
+ # authentication identity is established for the client by the
49
+ # external credentials.
50
+ #
51
+ # Any other keyword parameters are quietly ignored.
52
+ def initialize(user = nil, authzid: nil, username: nil, **)
53
+ authzid ||= username || user
54
+ @authzid = authzid&.to_str&.encode "UTF-8"
55
+ if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
56
+ raise ArgumentError, "contains NULL"
57
+ end
58
+ @done = false
59
+ end
60
+
61
+ # :call-seq:
62
+ # initial_response? -> true
63
+ #
64
+ # +EXTERNAL+ can send an initial client response.
65
+ def initial_response?; true end
66
+
67
+ # Returns #authzid, or an empty string if there is no authzid.
68
+ def process(_)
69
+ authzid || ""
70
+ ensure
71
+ @done = true
72
+ end
73
+
74
+ # Returns true when the initial client response was sent.
75
+ #
76
+ # The authentication should not succeed unless this returns true, but it
77
+ # does *not* indicate success.
78
+ def done?; @done end
79
+
80
+ end
81
+ end
82
+ end
83
+ 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://www.rfc-editor.org/rfc/rfc5801],
9
+ # several different mechanisms start with a GS2 header:
10
+ # * +GS2-*+ --- RFC5801[https://www.rfc-editor.org/rfc/rfc5801]
11
+ # * +SCRAM-*+ --- RFC5802[https://www.rfc-editor.org/rfc/rfc5802]
12
+ # (ScramAuthenticator)
13
+ # * +SAML20+ --- RFC6595[https://www.rfc-editor.org/rfc/rfc6595]
14
+ # * +OPENID20+ --- RFC6616[https://www.rfc-editor.org/rfc/rfc6616]
15
+ # * +OAUTH10A+ --- RFC7628[https://www.rfc-editor.org/rfc/rfc7628]
16
+ # * +OAUTHBEARER+ --- RFC7628[https://www.rfc-editor.org/rfc/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
@@ -3,9 +3,9 @@
3
3
  # Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate.
4
4
  #
5
5
  # +LOGIN+ authentication sends the password in cleartext.
6
- # RFC3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
6
+ # RFC3501[https://www.rfc-editor.org/rfc/rfc3501] encourages servers to disable
7
7
  # cleartext authentication until after TLS has been negotiated.
8
- # RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
8
+ # RFC8314[https://www.rfc-editor.org/rfc/rfc8314] recommends TLS version 1.2 or
9
9
  # greater be used for all traffic, and deprecate cleartext access ASAP. +LOGIN+
10
10
  # can be secured by TLS encryption.
11
11
  #
@@ -17,30 +17,40 @@
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
20
+ class Net::IMAP::SASL::LoginAuthenticator
21
+ STATE_USER = :USER
22
+ STATE_PASSWORD = :PASSWORD
23
+ STATE_DONE = :DONE
24
+ private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
25
+
26
+ def initialize(user = nil, pass = nil,
27
+ authcid: nil, username: nil,
28
+ password: nil, secret: nil,
29
+ warn_deprecation: true,
30
+ **)
31
+ if warn_deprecation
32
+ warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.",
33
+ category: :deprecated
34
+ end
35
+ @user = authcid || username || user
36
+ @password = password || secret || pass
37
+ @state = STATE_USER
38
+ end
39
+
40
+ def initial_response?; false end
41
+
21
42
  def process(data)
22
43
  case @state
23
44
  when STATE_USER
24
45
  @state = STATE_PASSWORD
25
46
  return @user
26
47
  when STATE_PASSWORD
48
+ @state = STATE_DONE
27
49
  return @password
50
+ when STATE_DONE
51
+ raise ResponseParseError, data
28
52
  end
29
53
  end
30
54
 
31
- private
32
-
33
- STATE_USER = :USER
34
- STATE_PASSWORD = :PASSWORD
35
-
36
- def initialize(user, password, warn_deprecation: true, **_ignored)
37
- if warn_deprecation
38
- warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
39
- end
40
- @user = user
41
- @password = password
42
- @state = STATE_USER
43
- end
44
-
45
- Net::IMAP.add_authenticator "LOGIN", self
55
+ def done?; @state == STATE_DONE end
46
56
  end