From 8316d446360d5f9523d5625a9ca5148d52de52d7 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 25 Jun 2024 22:18:13 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Enforce=20`LOGINDISABLED`=20requ?= =?UTF-8?q?irement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This may be considered a "breaking change", but it should have no negative effect on well behaved servers. This should merely change a NoResponseError into a LoginDisabledError. However, some broken servers have been known to hang indefinitely when issued a `CAPABILITY` command prior to authentication. For those servers, we offer the `enforce_logindisabled` config option. Fixes #32. --- lib/net/imap.rb | 21 +++-- lib/net/imap/config.rb | 28 ++++++ lib/net/imap/errors.rb | 6 ++ test/net/imap/test_imap_capabilities.rb | 2 +- test/net/imap/test_imap_login.rb | 119 ++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 test/net/imap/test_imap_login.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 6a55c5f2..bc6b7bea 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1378,13 +1378,9 @@ def authenticate(mechanism, *creds, # ===== Capabilities # # An IMAP client MUST NOT call #login when the server advertises the - # +LOGINDISABLED+ capability. - # - # if imap.capability? "LOGINDISABLED" - # raise "Remote server has disabled the login command" - # else - # imap.login username, password - # end + # +LOGINDISABLED+ capability. By default, Net::IMAP will raise a + # LoginDisabledError when that capability is present. See + # Config#enforce_logindisabled. # # Server capabilities may change after #starttls, #login, and #authenticate. # Cached capabilities _must_ be invalidated after this method completes. @@ -1392,6 +1388,9 @@ def authenticate(mechanism, *creds, # ResponseCode. # def login(user, password) + if enforce_logindisabled? && capability?("LOGINDISABLED") + raise LoginDisabledError + end send_command("LOGIN", user, password) .tap { @capabilities = capabilities_from_resp_code _1 } end @@ -2869,6 +2868,14 @@ def put_string(str) end end + def enforce_logindisabled? + if config.enforce_logindisabled == :when_capabilities_cached + capabilities_cached? + else + config.enforce_logindisabled + end + end + def search_internal(cmd, keys, charset) if keys.instance_of?(String) keys = [RawData.new(keys)] diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 221eb1b2..bcf5b72d 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -213,6 +213,32 @@ def self.[](config) # | v0.4 | +true+ (support added) | attr_accessor :sasl_ir, type: :boolean + # :markup: markdown + # + # Controls the behavior of Net::IMAP#login when the `LOGINDISABLED` + # capability is present. When enforced, Net::IMAP will raise a + # LoginDisabledError when that capability is present. Valid values are: + # + # [+false+] + # Send the +LOGIN+ command without checking for +LOGINDISABLED+. + # + # [+:when_capabilities_cached+] + # Enforce the requirement when Net::IMAP#capabilities_cached? is true, + # but do not send a +CAPABILITY+ command to discover the capabilities. + # + # [+true+] + # Only send the +LOGIN+ command if the +LOGINDISABLED+ capability is not + # present. When capabilities are unknown, Net::IMAP will automatically + # send a +CAPABILITY+ command first before sending +LOGIN+. + # + # | Starting with version | The default value is | + # |-------------------------|--------------------------------| + # | _original_ | `false` | + # | v0.5 | `true` | + attr_accessor :enforce_logindisabled, type: [ + false, :when_capabilities_cached, true + ] + # :markup: markdown # # Controls the behavior of Net::IMAP#responses when called without a @@ -306,6 +332,7 @@ def defaults_hash open_timeout: 30, idle_response_timeout: 5, sasl_ir: true, + enforce_logindisabled: true, responses_without_block: :warn, ).freeze @@ -317,6 +344,7 @@ def defaults_hash version_defaults[0] = Config[:current].dup.update( sasl_ir: false, responses_without_block: :silence_deprecation_warning, + enforce_logindisabled: false, ).freeze version_defaults[0.0] = Config[0] version_defaults[0.1] = Config[0] diff --git a/lib/net/imap/errors.rb b/lib/net/imap/errors.rb index a9d929ce..d329a513 100644 --- a/lib/net/imap/errors.rb +++ b/lib/net/imap/errors.rb @@ -7,6 +7,12 @@ class IMAP < Protocol class Error < StandardError end + class LoginDisabledError < Error + def initialize(msg = "Remote server has disabled the LOGIN command", ...) + super + end + end + # Error raised when data is in the incorrect format. class DataFormatError < Error end diff --git a/test/net/imap/test_imap_capabilities.rb b/test/net/imap/test_imap_capabilities.rb index 252a7d9b..ca83a20f 100644 --- a/test/net/imap/test_imap_capabilities.rb +++ b/test/net/imap/test_imap_capabilities.rb @@ -259,7 +259,7 @@ def teardown end test "#capabilities cache is NOT cleared after #login fails" do - with_fake_server(preauth: false, cleartext_auth: true) do |server, imap| + with_fake_server(preauth: false, cleartext_login: true) do |server, imap| original_capabilities = imap.capabilities begin imap.login("wrong_user", "wrong-password") diff --git a/test/net/imap/test_imap_login.rb b/test/net/imap/test_imap_login.rb new file mode 100644 index 00000000..f0d58da0 --- /dev/null +++ b/test/net/imap/test_imap_login.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" +require_relative "fake_server" + +class IMAPLoginTest < Test::Unit::TestCase + include Net::IMAP::FakeServer::TestHelper + + def setup + Net::IMAP.config.reset + @do_not_reverse_lookup = Socket.do_not_reverse_lookup + Socket.do_not_reverse_lookup = true + @threads = [] + end + + def teardown + if !@threads.empty? + assert_join_threads(@threads) + end + ensure + Socket.do_not_reverse_lookup = @do_not_reverse_lookup + end + + test "#login doesn't send CAPABILITY when it is already cached" do + with_fake_server( + preauth: false, cleartext_login: true, greeting_capabilities: true + ) do |server, imap| + imap.login("test_user", "test-password") + cmd = server.commands.pop + assert_equal "LOGIN", cmd.name + assert_empty server.commands + end + end + + test "#login raises LoginDisabledError when LOGINDISABLED" do + with_fake_server(preauth: false, cleartext_login: false) do |server, imap| + assert imap.capabilities_cached? + assert_raise(Net::IMAP::LoginDisabledError) do + imap.login("test_user", "test-password") + end + assert_empty server.commands + end + end + + test "#login first checks capabilities for LOGINDISABLED (success)" do + with_fake_server( + preauth: false, cleartext_login: true, greeting_capabilities: false + ) do |server, imap| + imap.login("test_user", "test-password") + cmd = server.commands.pop + assert_equal "CAPABILITY", cmd.name + cmd = server.commands.pop + assert_equal "LOGIN", cmd.name + assert_empty server.commands + end + end + + test "#login first checks capabilities for LOGINDISABLED (failure)" do + with_fake_server( + preauth: false, cleartext_login: false, greeting_capabilities: false + ) do |server, imap| + assert_raise(Net::IMAP::LoginDisabledError) do + imap.login("test_user", "test-password") + end + cmd = server.commands.pop + assert_equal "CAPABILITY", cmd.name + assert_empty server.commands + end + end + + test("#login sends LOGIN without asking CAPABILITY " \ + "when config.enforce_logindisabled is false") do + with_fake_server( + preauth: false, cleartext_login: false, greeting_capabilities: false + ) do |server, imap| + imap.config.enforce_logindisabled = false + imap.login("test_user", "test-password") + cmd = server.commands.pop + assert_equal "LOGIN", cmd.name + end + end + + test("#login raises LoginDisabledError without sending CAPABILITY " \ + "when config.enforce_logindisabled is :when_capabilities_cached") do + with_fake_server( + preauth: false, cleartext_login: false, greeting_capabilities: true + ) do |server, imap| + imap.config.enforce_logindisabled = :when_capabilities_cached + assert_raise(Net::IMAP::LoginDisabledError) do + imap.login("test_user", "test-password") + end + assert_empty server.commands + end + end + + test("#login sends LOGIN without asking CAPABILITY " \ + "when config.enforce_logindisabled is :when_capabilities_cached") do + with_fake_server( + preauth: false, cleartext_login: false, greeting_capabilities: false + ) do |server, imap| + imap.config.enforce_logindisabled = :when_capabilities_cached + imap.login("test_user", "test-password") + cmd = server.commands.pop + assert_equal "LOGIN", cmd.name + assert_empty server.commands + end + with_fake_server( + preauth: false, cleartext_login: true, greeting_capabilities: true + ) do |server, imap| + imap.config.enforce_logindisabled = :when_capabilities_cached + imap.login("test_user", "test-password") + cmd = server.commands.pop + assert_equal "LOGIN", cmd.name + assert_empty server.commands + end + end + +end