Skip to content

Commit 43b4034

Browse files
committed
✨ SASL ANONYMOUS: Add new mechanism
1 parent d0525de commit 43b4034

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

lib/net/imap/sasl.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ class IMAP
3838
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
3939
# supported.
4040
#
41+
# +ANONYMOUS+:: See AnonymousAuthenticator.
42+
# Allow the user to gain access to public services or
43+
# resources without authenticating or disclosing an
44+
# identity.
45+
#
4146
# === Deprecated mechanisms
4247
#
4348
# <em>Obsolete mechanisms are available for backwards compatibility. Using
@@ -62,6 +67,7 @@ module SASL
6267
sasl_dir = File.expand_path("sasl", __dir__)
6368
autoload :Authenticator, "#{sasl_dir}/authenticator"
6469
autoload :Authenticators, "#{sasl_dir}/authenticators"
70+
autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
6571
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
6672
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
6773

@@ -72,6 +78,7 @@ module SASL
7278
# Authenticators are all lazy loaded
7379
def self.authenticators
7480
@authenticators ||= SASL::Authenticators.new.tap do |registry|
81+
registry.add_authenticator "Anonymous"
7582
registry.add_authenticator "Plain"
7683
registry.add_authenticator "XOAuth2"
7784
registry.add_authenticator "Login" # deprecated
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "authenticator"
4+
5+
module Net
6+
class IMAP < Protocol
7+
module SASL
8+
9+
# Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by
10+
# RFC-4505[https://tools.ietf.org/html/rfc4505]. See
11+
# Net::IMAP#authenticate.
12+
class AnonymousAuthenticator < Authenticator
13+
14+
# :call-seq:
15+
# initial_response? -> true
16+
#
17+
# +ANONYMOUS+ can send an initial client response.
18+
def initial_response?; true end
19+
20+
##
21+
# :call-seq:
22+
# new -> authenticator
23+
# new(anonymous_message, **) -> authenticator
24+
# new(anonymous_message:, **) -> authenticator
25+
# new(message:, **) -> authenticator
26+
# new {|propname, auth_ctx| propval } -> authenticator
27+
#
28+
# Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
29+
# specified in RFC-4505[https://tools.ietf.org/html/rfc4505]. To use
30+
# this, see Net::IMAP#authenticate or your client's authentication
31+
# method.
32+
#
33+
# ==== Properties
34+
# Only one optional property:
35+
#
36+
# * #anonymous_message --- an optional message sent to the server which
37+
# doesn't contain an <tt>"@"</tt> character, or if it does have an
38+
# <tt>"@"</tt> it must be a valid email address.
39+
#
40+
# May be sent as positional argument or as a keyword argument.
41+
# Aliased as #message.
42+
#
43+
# See Net::IMAP::SASL::Authenticator@Properties for a detailed
44+
# description of property assignment, lazy loading, and callbacks.
45+
def initialize(message_arg = nil, anonymous_message: nil, message: nil)
46+
super
47+
propinit :anonymous_message, message_arg, anonymous_message, message
48+
end
49+
50+
##
51+
# method: anonymous_message
52+
# :call-seq:
53+
# anonymous_message -> string or nil
54+
#
55+
# A token sent for the +ANONYMOUS+ mechanism.
56+
#
57+
# Restricted to 255 UTF8 encoded characters, which will be validated by
58+
# #process.
59+
#
60+
# If an "@" sign is included, the message must be a valid email address
61+
# (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
62+
# Email syntax will _not_ be validated by AnonymousAuthenticator.
63+
#
64+
# Otherwise, it can be any UTF8 string which is permitted by the
65+
# StringPrep "+trace+" profile. This is validated by #process.
66+
# See AnonymousAuthenticator.stringprep_trace.
67+
property :anonymous_message
68+
alias message anonymous_message
69+
70+
# From RFC-4505[https://tools.ietf.org/html/rfc4505] §3, The "trace"
71+
# Profile of "Stringprep":
72+
# >>>
73+
# Characters from the following tables of [StringPrep] are prohibited:
74+
#
75+
# - C.2.1 (ASCII control characters)
76+
# - C.2.2 (Non-ASCII control characters)
77+
# - C.3 (Private use characters)
78+
# - C.4 (Non-character code points)
79+
# - C.5 (Surrogate codes)
80+
# - C.6 (Inappropriate for plain text)
81+
# - C.8 (Change display properties are deprecated)
82+
# - C.9 (Tagging characters)
83+
#
84+
# No additional characters are prohibited.
85+
SASLPREP_TRACE_TABLES = %w[C.2.1 C.2.2 C.3 C.4 C.5 C.6 C.8 C.9].freeze
86+
87+
# From RFC-4505[https://tools.ietf.org/html/rfc4505] §3, The "trace"
88+
# Profile of "Stringprep":
89+
# >>>
90+
# The character repertoire of this profile is Unicode 3.2 [Unicode].
91+
#
92+
# No mapping is required by this profile.
93+
#
94+
# No Unicode normalization is required by this profile.
95+
#
96+
# The list of unassigned code points for this profile is that provided
97+
# in Appendix A of [StringPrep]. Unassigned code points are not
98+
# prohibited.
99+
#
100+
# Characters from the following tables of [StringPrep] are prohibited:
101+
# (documented on SASLPREP_TRACE_TABLES)
102+
#
103+
# This profile requires bidirectional character checking per Section 6
104+
# of [StringPrep].
105+
def self.stringprep_trace(string)
106+
StringPrep.check_prohibited!(string,
107+
*SASLPREP_TRACE_TABLES,
108+
bidi: true,
109+
profile: "trace")
110+
string
111+
end
112+
113+
# Returns the #anonymous_message, after checking it with
114+
# rdoc-ref:AnonymousAuthenticator.stringprep_trace.
115+
def process(_server_challenge_string)
116+
if (size = anonymous_message&.length)&.> 255
117+
raise Error, "anonymous_message is too long. (%d codepoints)" % [
118+
size
119+
]
120+
end
121+
self.class.stringprep_trace(anonymous_message || "")
122+
end
123+
124+
end
125+
end
126+
end
127+
end

test/net/imap/test_imap_authenticators.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,43 @@ def test_xoauth2_callbacks
8686
)
8787
end
8888

89+
# ----------------------
90+
# ANONYMOUS
91+
# ----------------------
92+
93+
def anonymous(*args, **kwargs, &block)
94+
Net::IMAP::SASL.authenticator("ANONYMOUS", *args, **kwargs, &block)
95+
end
96+
97+
def test_anonymous_matches_mechanism
98+
assert_kind_of(Net::IMAP::SASL::AnonymousAuthenticator, anonymous)
99+
end
100+
101+
def test_anonymous_response
102+
assert_equal("", anonymous.process(nil))
103+
assert_equal("hello world", anonymous("hello world").process(nil))
104+
assert_equal("kwargs",
105+
anonymous(anonymous_message: "kwargs").process(nil))
106+
end
107+
108+
def test_anonymous_stringprep
109+
assert_raise(Net::IMAP::SASL::ProhibitedCodepoint) {
110+
anonymous("no\ncontrol\rchars").process(nil)
111+
}
112+
assert_raise(Net::IMAP::SASL::ProhibitedCodepoint) {
113+
anonymous("regional flags use tagging chars: e.g." \
114+
"🏴󠁧󠁢󠁥󠁮󠁧󠁿 England, " \
115+
"🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland, " \
116+
"🏴󠁧󠁢󠁷󠁬󠁳󠁿 Wales.").process(nil)
117+
}
118+
end
119+
120+
def test_anonymous_length_over_255
121+
assert_raise(Net::IMAP::Error) {
122+
anonymous("a" * 256).process(nil)
123+
}
124+
end
125+
89126
# ----------------------
90127
# LOGIN (obsolete)
91128
# ----------------------

0 commit comments

Comments
 (0)