diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 72fb8b0d..7d205763 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1053,14 +1053,25 @@ def starttls(options = {}, verify = true) # If the TaggedResponse to #authenticate includes updated capabilities, they # will be cached. # - def authenticate(mechanism, ...) - authenticator = self.class.authenticator(mechanism, ...) - send_command("AUTHENTICATE", mechanism) do |resp| + def authenticate(mechanism, *creds, sasl_ir: false, **props, &callback) + [nil, true, false, :auto].include?(sasl_ir) or + raise ArgumentError, "sasl_ir must be boolean or :auto" + authenticator = self.class.authenticator(mechanism, + *creds, + **props, + &callback) + cmdargs = ["AUTHENTICATE", mechanism] + sasl_ir = capable?("SASL-IR") if [nil, :auto].include?(sasl_ir) + if sasl_ir && SASL.initial_response?(authenticator) + response = authenticator.process(nil) + cmdargs << [response].pack("m0") + end + send_command(*cmdargs) do |resp| if resp.instance_of?(ContinuationRequest) - data = authenticator.process(resp.data.text.unpack("m")[0]) - s = [data].pack("m0") - send_string_data(s) - put_string(CRLF) + challenge = resp.data.text.unpack1("m") + response = authenticator.process(challenge) + response = [response].pack("m0") + put_string(response + CRLF) end end .tap { @capabilities = capabilities_from_resp_code _1 } diff --git a/lib/net/imap/authenticators/plain.rb b/lib/net/imap/authenticators/plain.rb index a9d46c92..e7fe07c4 100644 --- a/lib/net/imap/authenticators/plain.rb +++ b/lib/net/imap/authenticators/plain.rb @@ -11,6 +11,8 @@ # can be secured by TLS encryption. class Net::IMAP::PlainAuthenticator + def initial_response?; true end + def process(data) return "#@authzid\0#@username\0#@password" end diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb index 65858072..eca77bdd 100644 --- a/lib/net/imap/sasl.rb +++ b/lib/net/imap/sasl.rb @@ -39,6 +39,10 @@ def saslprep(string, **opts) Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts) end + def initial_response?(mechanism) + mechanism.respond_to?(:initial_response?) && mechanism.initial_response? + end + end end diff --git a/test/net/imap/fake_server/command_reader.rb b/test/net/imap/fake_server/command_reader.rb index 2abe2f8a..2fd15ef2 100644 --- a/test/net/imap/fake_server/command_reader.rb +++ b/test/net/imap/fake_server/command_reader.rb @@ -32,7 +32,7 @@ def get_command def parse(buf) /\A([^ ]+) ((?:UID )?\w+)(?: (.+))?\r\n\z/min =~ buf or raise "bad request" case $2.upcase - when "LOGIN", "SELECT", "ENABLE" + when "LOGIN", "SELECT", "ENABLE", "AUTHENTICATE" Command.new $1, $2, scan_astrings($3), buf else Command.new $1, $2, $3, buf # TODO... diff --git a/test/net/imap/fake_server/command_router.rb b/test/net/imap/fake_server/command_router.rb index c99a0a98..96996148 100644 --- a/test/net/imap/fake_server/command_router.rb +++ b/test/net/imap/fake_server/command_router.rb @@ -79,8 +79,14 @@ def handler_for(command) on "AUTHENTICATE" do |resp| state.not_authenticated? or return resp.fail_bad_state(state) args = resp.command.args - args == "PLAIN" or return resp.fail_no "unsupported" - response_b64 = resp.request_continuation("") || "" + (1..2) === args.length or return resp.fail_bad_args + args.first == "PLAIN" or return resp.fail_no "unsupported" + if args.length == 2 + response_b64 = args.last + else + response_b64 = resp.request_continuation("") || "" + state.commands << {continuation: response_b64} + end response = Base64.decode64(response_b64) response.empty? and return resp.fail_bad "canceled" # TODO: support mechanisms other than PLAIN. diff --git a/test/net/imap/fake_server/configuration.rb b/test/net/imap/fake_server/configuration.rb index b215f019..91fe72a4 100644 --- a/test/net/imap/fake_server/configuration.rb +++ b/test/net/imap/fake_server/configuration.rb @@ -22,6 +22,7 @@ class Configuration encrypted_login: true, cleartext_auth: false, sasl_mechanisms: %i[PLAIN].freeze, + sasl_ir: false, rev1: true, rev2: false, @@ -66,6 +67,7 @@ def initialize(with_extensions: [], without_extensions: [], **opts, &block) alias cleartext_auth? cleartext_auth alias greeting_bye? greeting_bye alias greeting_capabilities? greeting_capabilities + alias sasl_ir? sasl_ir def on(event, &handler) handler or raise ArgumentError @@ -104,6 +106,7 @@ def capabilities_pre_tls capa << "STARTTLS" if starttls? capa << "LOGINDISABLED" unless cleartext_login? capa.concat auth_capabilities if cleartext_auth? + capa << "SASL-IR" if sasl_ir? && cleartext_auth? capa end @@ -111,6 +114,7 @@ def capabilities_pre_auth capa = basic_capabilities capa << "LOGINDISABLED" unless encrypted_login? capa.concat auth_capabilities + capa << "SASL-IR" if sasl_ir? capa end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 94971022..92c1d585 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -776,6 +776,47 @@ def test_id end end + test "#authenticate PLAIN (sasl_ir: default)" do + with_fake_server(preauth: false, cleartext_auth: true) do |server, imap| + imap.authenticate("PLAIN", "test_user", "test-password") + cmd, continuation = server.commands.pop, server.commands.pop + assert_empty server.commands + assert_equal "AUTHENTICATE", cmd.name + assert_equal ["PLAIN"], cmd.args + encoded = continuation[:continuation] + assert_equal "\x00test_user\x00test-password", Base64.decode64(encoded) + end + end + + test "#authenticate(..., sasl_ir: :auto), SASL-IR not in capabilities" do + with_fake_server(preauth: false, cleartext_auth: true) do |server, imap| + refute imap.capable? "SASL-IR" + imap.authenticate("PLAIN", "test_user", "test-password", sasl_ir: :auto) + cmd, continuation = server.commands.pop, server.commands.pop + assert_empty server.commands + assert_equal "AUTHENTICATE", cmd.name + assert_equal ["PLAIN"], cmd.args + encoded = continuation[:continuation] + assert_equal "\x00test_user\x00test-password", Base64.decode64(encoded) + end + end + + test "#authenticate(..., sasl_ir: :auto), SASL-IR in capabilities" do + with_fake_server( + preauth: false, cleartext_auth: true, sasl_ir: true + ) do |server, imap| + assert imap.capable? "SASL-IR" + imap.authenticate("PLAIN", "test_user", "test-password", sasl_ir: :auto) + cmd = server.commands.pop + assert_empty server.commands + assert_equal "AUTHENTICATE", cmd.name + assert_equal( + ["PLAIN", ["\x00test_user\x00test-password"].pack("m0")], + cmd.args + ) + end + end + def test_uidplus_uid_expunge with_fake_server(select: "INBOX", extensions: %i[UIDPLUS]) do |server, imap| diff --git a/test/net/imap/test_imap_capabilities.rb b/test/net/imap/test_imap_capabilities.rb index 31f4d0ff..14e1f79b 100644 --- a/test/net/imap/test_imap_capabilities.rb +++ b/test/net/imap/test_imap_capabilities.rb @@ -190,6 +190,7 @@ def teardown imap.authenticate("PLAIN", "test_user", "test-password") assert_equal "AUTHENTICATE", server.commands.pop.name + assert server.commands.pop[:continuation] refute imap.capabilities_cached? assert imap.capable? :IMAP4rev1 @@ -277,6 +278,7 @@ def teardown rescue Net::IMAP::NoResponseError end assert_equal "AUTHENTICATE", server.commands.pop.name + assert server.commands.pop[:continuation] assert_equal original_capabilities, imap.capabilities assert_empty server.commands end