Skip to content

Commit aed3a57

Browse files
authored
🔀 Merge pull request #165 from nevans/move-and-rename-sasl-authenticators
🚚 Move and rename sasl authenticators
2 parents 4520aa8 + 0e1e416 commit aed3a57

11 files changed

+171
-82
lines changed

‎lib/net/imap.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ def starttls(options = {}, verify = true)
10151015
# +PLAIN+:: See PlainAuthenticator.
10161016
# Login using clear-text username and password.
10171017
#
1018-
# +XOAUTH2+:: See XOauth2Authenticator.
1018+
# +XOAUTH2+:: See XOAuth2Authenticator.
10191019
# Login using a username and OAuth2 access token.
10201020
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
10211021
# supported.
@@ -1074,10 +1074,7 @@ def starttls(options = {}, verify = true)
10741074
# completes. If the TaggedResponse to #authenticate includes updated
10751075
# capabilities, they will be cached.
10761076
def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback)
1077-
authenticator = self.class.authenticator(mechanism,
1078-
*creds,
1079-
**props,
1080-
&callback)
1077+
authenticator = SASL.authenticator(mechanism, *creds, **props, &callback)
10811078
cmdargs = ["AUTHENTICATE", mechanism]
10821079
if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) &&
10831080
SASL.initial_response?(authenticator)

‎lib/net/imap/authenticators.rb

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,37 @@
11
# frozen_string_literal: true
22

3-
# Registry for SASL authenticators used by Net::IMAP.
3+
# Backward compatible delegators from Net::IMAP to Net::IMAP::SASL.
44
module Net::IMAP::Authenticators
55

6-
# Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
7-
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
8-
# implemented by +authenticator+ (for instance, <tt>"PLAIN"</tt>).
9-
#
10-
# The +authenticator+ must respond to +#new+ (or #call), receiving the
11-
# authenticator configuration and return a configured authentication session.
12-
# The authenticator session must respond to +#process+, receiving the server's
13-
# challenge and returning the client's response.
14-
#
15-
# See PlainAuthenticator, XOauth2Authenticator, and DigestMD5Authenticator for
16-
# examples.
17-
def add_authenticator(auth_type, authenticator)
18-
authenticators[auth_type] = authenticator
6+
# Deprecated. Use Net::IMAP::SASL.add_authenticator instead.
7+
def add_authenticator(...)
8+
warn(
9+
"%s.%s is deprecated. Use %s.%s instead." % [
10+
Net::IMAP, __method__, Net::IMAP::SASL, __method__
11+
],
12+
uplevel: 1
13+
)
14+
Net::IMAP::SASL.add_authenticator(...)
1915
end
2016

21-
# :call-seq:
22-
# authenticator(mechanism, ...) -> authenticator
23-
# authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
24-
# authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator
25-
# authenticator(mechanism, **properties) -> authenticator
26-
# authenticator(mechanism) {|propname, authctx| value } -> authenticator
27-
#
28-
# Builds a new authentication session context for +mechanism+.
29-
#
30-
# [Note]
31-
# This method is intended for internal use by connection protocol code only.
32-
# Protocol client users should see refer to their client's documentation,
33-
# e.g. Net::IMAP#authenticate for Net::IMAP.
34-
#
35-
# The call signatures documented for this method are recommendations for
36-
# authenticator implementors. All arguments (other than +mechanism+) are
37-
# forwarded to the registered authenticator's +#new+ (or +#call+) method, and
38-
# each authenticator must document its own arguments.
39-
#
40-
# The returned object represents a single authentication exchange and <em>must
41-
# not</em> be reused for multiple authentication attempts.
42-
def authenticator(mechanism, ...)
43-
auth = authenticators.fetch(mechanism.upcase) do
44-
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
45-
end
46-
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
47-
end
48-
49-
private
50-
51-
def authenticators
52-
@authenticators ||= {}
17+
# Deprecated. Use Net::IMAP::SASL.authenticator instead.
18+
def authenticator(...)
19+
warn(
20+
"%s.%s is deprecated. Use %s.%s instead." % [
21+
Net::IMAP, __method__, Net::IMAP::SASL, __method__
22+
],
23+
uplevel: 1
24+
)
25+
Net::IMAP::SASL.authenticator(...)
5326
end
5427

28+
Net::IMAP.extend self
5529
end
5630

57-
Net::IMAP.extend Net::IMAP::Authenticators
31+
class Net::IMAP
32+
PlainAuthenticator = SASL::PlainAuthenticator # :nodoc:
33+
deprecate_constant :PlainAuthenticator
5834

59-
require_relative "authenticators/plain"
60-
61-
require_relative "authenticators/login"
62-
require_relative "authenticators/cram_md5"
63-
require_relative "authenticators/digest_md5"
64-
require_relative "authenticators/xoauth2"
35+
XOauth2Authenticator = SASL::XOAuth2Authenticator # :nodoc:
36+
deprecate_constant :XOauth2Authenticator
37+
end

‎lib/net/imap/sasl.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ module SASL
3232
autoload :ProhibitedCodepoint, sasl_stringprep_rb
3333
autoload :BidiStringError, sasl_stringprep_rb
3434

35+
sasl_dir = File.expand_path("sasl", __dir__)
36+
autoload :Authenticators, "#{sasl_dir}/authenticators"
37+
38+
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
39+
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
40+
41+
autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"
42+
autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator"
43+
autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"
44+
45+
# Returns the default global SASL::Authenticators instance.
46+
def self.authenticators
47+
@authenticators ||= Authenticators.new(use_defaults: true)
48+
end
49+
50+
# Delegates to ::authenticators. See Authenticators#authenticator.
51+
def self.authenticator(...) authenticators.authenticator(...) end
52+
53+
# Delegates to ::authenticators. See Authenticators#add_authenticator.
54+
def self.add_authenticator(...) authenticators.add_authenticator(...) end
55+
3556
module_function
3657

3758
# See Net::IMAP::StringPrep::SASLprep#saslprep.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
# By default, the registry will be empty--without any registrations. When
30+
# +add_defaults+ is +true+, authenticators for all standard mechanisms will
31+
# be registered.
32+
#
33+
def initialize(use_defaults: false)
34+
@authenticators = {}
35+
if use_defaults
36+
add_authenticator "PLAIN", PlainAuthenticator
37+
add_authenticator "XOAUTH2", XOAuth2Authenticator
38+
add_authenticator "LOGIN", LoginAuthenticator # deprecated
39+
add_authenticator "CRAM-MD5", CramMD5Authenticator # deprecated
40+
add_authenticator "DIGEST-MD5", DigestMD5Authenticator # deprecated
41+
end
42+
end
43+
44+
# Returns the names of all registered SASL mechanisms.
45+
def names; @authenticators.keys end
46+
47+
# :call-seq:
48+
# add_authenticator(mechanism)
49+
# add_authenticator(mechanism, authenticator_class)
50+
# add_authenticator(mechanism, authenticator_proc)
51+
#
52+
# Registers an authenticator for #authenticator to use. +mechanism+ is the
53+
# name of the
54+
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
55+
# implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
56+
#
57+
# If +mechanism+ refers to an existing authenticator, a warning will be
58+
# printed and the old authenticator will be replaced.
59+
#
60+
# When only a single argument is given, the authenticator class will be
61+
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
62+
# preserved and non-alphanumeric characters are removed..
63+
def add_authenticator(auth_type, authenticator)
64+
@authenticators[auth_type] = authenticator
65+
end
66+
67+
# :call-seq:
68+
# authenticator(mechanism, ...) -> auth_session
69+
#
70+
# Builds an authenticator instance using the authenticator registered to
71+
# +mechanism+. The returned object represents a single authentication
72+
# exchange and <em>must not</em> be reused for multiple authentication
73+
# attempts.
74+
#
75+
# All arguments (except +mechanism+) are forwarded to the registered
76+
# authenticator's +#new+ or +#call+ method. Each authenticator must
77+
# document its own arguments.
78+
#
79+
# [Note]
80+
# This method is intended for internal use by connection protocol code
81+
# only. Protocol client users should see refer to their client's
82+
# documentation, e.g. Net::IMAP#authenticate.
83+
def authenticator(mechanism, ...)
84+
auth = @authenticators.fetch(mechanism.upcase) do
85+
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
86+
end
87+
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
88+
end
89+
90+
end
91+
92+
end

‎lib/net/imap/authenticators/cram_md5.rb renamed to ‎lib/net/imap/sasl/cram_md5_authenticator.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
1414
# of cleartext and recommends TLS version 1.2 or greater be used for all
1515
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
16-
class Net::IMAP::CramMD5Authenticator
16+
class Net::IMAP::SASL::CramMD5Authenticator
1717
def process(challenge)
1818
digest = hmac_md5(challenge, @password)
1919
return @user + " " + digest
@@ -47,5 +47,4 @@ def hmac_md5(text, key)
4747
return Digest::MD5.hexdigest(k_opad + digest)
4848
end
4949

50-
Net::IMAP.add_authenticator "CRAM-MD5", self
5150
end

‎lib/net/imap/authenticators/digest_md5.rb renamed to ‎lib/net/imap/sasl/digest_md5_authenticator.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# "+DIGEST-MD5+" has been deprecated by
99
# {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be relied on for
1010
# security. It is included for compatibility with existing servers.
11-
class Net::IMAP::DigestMD5Authenticator
11+
class Net::IMAP::SASL::DigestMD5Authenticator
1212
def process(challenge)
1313
case @stage
1414
when STAGE_ONE
@@ -111,5 +111,4 @@ def qdval(k, v)
111111
end
112112
end
113113

114-
Net::IMAP.add_authenticator "DIGEST-MD5", self
115114
end

‎lib/net/imap/authenticators/login.rb renamed to ‎lib/net/imap/sasl/login_authenticator.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# compatibility with existing servers. See
1818
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
1919
# for both specification and deprecation.
20-
class Net::IMAP::LoginAuthenticator
20+
class Net::IMAP::SASL::LoginAuthenticator
2121
def process(data)
2222
case @state
2323
when STATE_USER
@@ -42,5 +42,4 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
4242
@state = STATE_USER
4343
end
4444

45-
Net::IMAP.add_authenticator "LOGIN", self
4645
end

‎lib/net/imap/authenticators/plain.rb renamed to ‎lib/net/imap/sasl/plain_authenticator.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
1010
# greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+
1111
# can be secured by TLS encryption.
12-
class Net::IMAP::PlainAuthenticator
12+
class Net::IMAP::SASL::PlainAuthenticator
1313

1414
def initial_response?; true end
1515

@@ -39,5 +39,4 @@ def initialize(username, password, authzid: nil)
3939
@authzid = authzid
4040
end
4141

42-
Net::IMAP.add_authenticator "PLAIN", self
4342
end

‎lib/net/imap/authenticators/xoauth2.rb renamed to ‎lib/net/imap/sasl/xoauth2_authenticator.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class Net::IMAP::XOauth2Authenticator
3+
class Net::IMAP::SASL::XOAuth2Authenticator
44

55
def initial_response?; true end
66

@@ -19,5 +19,4 @@ def build_oauth2_string(user, oauth2_token)
1919
format("user=%s\1auth=Bearer %s\1\1", user, oauth2_token)
2020
end
2121

22-
Net::IMAP.add_authenticator 'XOAUTH2', self
2322
end

0 commit comments

Comments
 (0)