From c5c4b9ec7db263667f0938acd5ca57ca17318135 Mon Sep 17 00:00:00 2001 From: TOMITA Masahiro Date: Mon, 17 Oct 2022 09:04:51 +0900 Subject: [PATCH] Make the digest library optional --- lib/net/smtp.rb | 21 ++- test/net/smtp/test_smtp.rb | 263 +++++++++++++++++++++++++------------ 2 files changed, 197 insertions(+), 87 deletions(-) diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index 240e540..b2c1bee 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -18,10 +18,13 @@ # require 'net/protocol' -require 'digest/md5' begin require 'openssl' rescue LoadError + begin + require 'digest/md5' + rescue LoadError + end end module Net @@ -625,6 +628,16 @@ def finish private + def digest_class + @digest_class ||= if defined?(OpenSSL::Digest) + OpenSSL::Digest + elsif defined?(::Digest) + ::Digest + else + raise '"openssl" or "digest" library is required' + end + end + def tcp_socket(address, port) begin Socket.tcp address, port, nil, nil, connect_timeout: @open_timeout @@ -887,14 +900,14 @@ def base64_encode(str) # CRAM-MD5: [RFC2195] def cram_md5_response(secret, challenge) - tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge) - Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) + tmp = digest_class::MD5.digest(cram_secret(secret, IMASK) + challenge) + digest_class::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) end CRAM_BUFSIZE = 64 def cram_secret(secret, mask) - secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE + secret = digest_class::MD5.digest(secret) if secret.size > CRAM_BUFSIZE buf = secret.ljust(CRAM_BUFSIZE, "\0") 0.upto(buf.size - 1) do |i| buf[i] = (buf[i].ord ^ mask).chr diff --git a/test/net/smtp/test_smtp.rb b/test/net/smtp/test_smtp.rb index 3e87dad..b418b3f 100644 --- a/test/net/smtp/test_smtp.rb +++ b/test/net/smtp/test_smtp.rb @@ -44,7 +44,7 @@ def initialize(*) end def teardown - @server_threads.each {|th| th.join } + @server_threads.each {|th| th.kill; th.join } end def test_critical @@ -71,11 +71,19 @@ def test_esmtp end def test_server_capabilities - port = fake_server_start(starttls: true) - smtp = Net::SMTP.start('localhost', port, starttls: false) - assert_equal({"STARTTLS"=>[], "AUTH"=>["PLAIN"]}, smtp.capabilities) - assert_equal(true, smtp.capable?('STARTTLS')) - assert_equal(false, smtp.capable?('SMTPUTF8')) + if defined? OpenSSL + port = fake_server_start(starttls: true) + smtp = Net::SMTP.start('localhost', port, starttls: false) + assert_equal({"STARTTLS"=>[], "AUTH"=>["PLAIN"]}, smtp.capabilities) + assert_equal(true, smtp.capable?('STARTTLS')) + assert_equal(false, smtp.capable?('SMTPUTF8')) + else + port = fake_server_start + smtp = Net::SMTP.start('localhost', port, starttls: false) + assert_equal({"AUTH"=>["PLAIN"]}, smtp.capabilities) + assert_equal(false, smtp.capable?('STARTTLS')) + assert_equal(false, smtp.capable?('SMTPUTF8')) + end smtp.finish end @@ -320,57 +328,63 @@ def test_eof_error_backtrace end end - if defined? OpenSSL::VERSION - def test_with_tls - port = fake_server_start(tls: true) - smtp = Net::SMTP.new('localhost', port, tls: true, tls_verify: false) - assert_nothing_raised do - smtp.start{} - end + def test_with_tls + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) - port = fake_server_start(tls: false) - smtp = Net::SMTP.new('localhost', port, tls: false) - assert_nothing_raised do - smtp.start{} - end + port = fake_server_start(tls: true) + smtp = Net::SMTP.new('localhost', port, tls: true, tls_verify: false) + assert_nothing_raised do + smtp.start{} end - def test_with_starttls_always - port = fake_server_start(starttls: true) - smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false) + port = fake_server_start(tls: false) + smtp = Net::SMTP.new('localhost', port, tls: false) + assert_nothing_raised do smtp.start{} - assert_equal(true, @starttls_started) - - port = fake_server_start(starttls: false) - smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false) - assert_raise Net::SMTPUnsupportedCommand do - smtp.start{} - end end + end - def test_with_starttls_auto - port = fake_server_start(starttls: true) - smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false) - smtp.start{} - assert_equal(true, @starttls_started) + def test_with_starttls_always + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) - port = fake_server_start(starttls: false) - smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false) + port = fake_server_start(starttls: true) + smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false) + smtp.start{} + assert_equal(true, @starttls_started) + + port = fake_server_start(starttls: false) + smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false) + assert_raise Net::SMTPUnsupportedCommand do smtp.start{} - assert_equal(false, @starttls_started) end + end - def test_with_starttls_false - port = fake_server_start(starttls: true) - smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false) - smtp.start{} - assert_equal(false, @starttls_started) + def test_with_starttls_auto + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) - port = fake_server_start(starttls: false) - smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false) - smtp.start{} - assert_equal(false, @starttls_started) - end + port = fake_server_start(starttls: true) + smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false) + smtp.start{} + assert_equal(true, @starttls_started) + + port = fake_server_start(starttls: false) + smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false) + smtp.start{} + assert_equal(false, @starttls_started) + end + + def test_with_starttls_false + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) + + port = fake_server_start(starttls: true) + smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false) + smtp.start{} + assert_equal(false, @starttls_started) + + port = fake_server_start(starttls: false) + smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false) + smtp.start{} + assert_equal(false, @starttls_started) end def test_start @@ -404,49 +418,110 @@ def test_start_invalid_number_of_arguments assert_equal('wrong number of arguments (given 7, expected 1..6)', err.message) end - if defined? OpenSSL::VERSION - def test_start_with_tls - port = fake_server_start(tls: true) - assert_nothing_raised do - Net::SMTP.start('localhost', port, tls: true, tls_verify: false){} - end + def test_start_with_tls + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) - port = fake_server_start(tls: false) - assert_nothing_raised do - Net::SMTP.start('localhost', port, tls: false){} - end + port = fake_server_start(tls: true) + assert_nothing_raised do + Net::SMTP.start('localhost', port, tls: true, tls_verify: false){} end - def test_start_with_starttls_always - port = fake_server_start(starttls: true) + port = fake_server_start(tls: false) + assert_nothing_raised do + Net::SMTP.start('localhost', port, tls: false){} + end + end + + def test_start_with_starttls_always + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) + + port = fake_server_start(starttls: true) + Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){} + assert_equal(true, @starttls_started) + + port = fake_server_start(starttls: false) + assert_raise Net::SMTPUnsupportedCommand do Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){} - assert_equal(true, @starttls_started) + end + end - port = fake_server_start(starttls: false) - assert_raise Net::SMTPUnsupportedCommand do - Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){} - end + def test_start_with_starttls_auto + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) + + port = fake_server_start(starttls: true) + Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){} + assert_equal(true, @starttls_started) + + port = fake_server_start(starttls: false) + Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){} + assert_equal(false, @starttls_started) + end + + def test_start_with_starttls_false + omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) + + port = fake_server_start(starttls: true) + Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){} + assert_equal(false, @starttls_started) + + port = fake_server_start(starttls: false) + Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){} + assert_equal(false, @starttls_started) + end + + def test_start_auth_plain + port = fake_server_start(user: 'account', password: 'password', authtype: 'PLAIN') + Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :plain){} + + port = fake_server_start(user: 'account', password: 'password', authtype: 'PLAIN') + assert_raise Net::SMTPAuthenticationError do + Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :plain){} end - def test_start_with_starttls_auto - port = fake_server_start(starttls: true) - Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){} - assert_equal(true, @starttls_started) + port = fake_server_start(user: 'account', password: 'password', authtype: 'LOGIN') + assert_raise Net::SMTPAuthenticationError do + Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :plain){} + end + end + + def test_start_auth_login + port = fake_server_start(user: 'account', password: 'password', authtype: 'LOGIN') + Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :login){} - port = fake_server_start(starttls: false) - Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){} - assert_equal(false, @starttls_started) + port = fake_server_start(user: 'account', password: 'password', authtype: 'LOGIN') + assert_raise Net::SMTPAuthenticationError do + Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :login){} end - def test_start_with_starttls_false - port = fake_server_start(starttls: true) - Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){} - assert_equal(false, @starttls_started) + port = fake_server_start(user: 'account', password: 'password', authtype: 'PLAIN') + assert_raise Net::SMTPAuthenticationError do + Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :login){} + end + end + + def test_start_auth_cram_md5 + omit "openssl or digest library not loaded" unless defined? OpenSSL or defined? Digest - port = fake_server_start(starttls: false) - Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){} - assert_equal(false, @starttls_started) + port = fake_server_start(user: 'account', password: 'password', authtype: 'CRAM-MD5') + Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :cram_md5){} + + port = fake_server_start(user: 'account', password: 'password', authtype: 'CRAM-MD5') + assert_raise Net::SMTPAuthenticationError do + Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :cram_md5){} + end + + port = fake_server_start(user: 'account', password: 'password', authtype: 'PLAIN') + assert_raise Net::SMTPAuthenticationError do + Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :cram_md5){} end + + port = fake_server_start(user: 'account', password: 'password', authtype: 'CRAM-MD5') + smtp = Net::SMTP.new('localhost', port) + smtp.define_singleton_method(:digest_class) { raise '"openssl" or "digest" library is required' } + e = assert_raise RuntimeError do + smtp.start(user: 'account', password: 'password', authtype: :cram_md5){} + end + assert_equal('"openssl" or "digest" library is required', e.message) end def test_start_instance @@ -491,7 +566,7 @@ def accept(servers) Socket.accept_loop(servers) { |s, _| break s } end - def fake_server_start(helo: 'localhost', user: nil, password: nil, tls: false, starttls: false) + def fake_server_start(helo: 'localhost', user: nil, password: nil, tls: false, starttls: false, authtype: 'PLAIN') @starttls_started = false servers = Socket.tcp_server_sockets('localhost', 0) @server_threads << Thread.start do @@ -515,7 +590,7 @@ def fake_server_start(helo: 'localhost', user: nil, password: nil, tls: false, s assert_equal(helo, comm.split[1]) sock.puts "220-servername\r\n" sock.puts "220-STARTTLS\r\n" if starttls - sock.puts "220 AUTH PLAIN\r\n" + sock.puts "220 AUTH #{authtype}\r\n" when "STARTTLS" unless starttls sock.puts "502 5.5.1 Error: command not implemented\r\n" @@ -526,14 +601,36 @@ def fake_server_start(helo: 'localhost', user: nil, password: nil, tls: false, s sock.sync_close = true sock.accept @starttls_started = true - when /\AAUTH PLAIN / + when /\AAUTH / unless user sock.puts "503 5.5.1 Error: authentication not enabled\r\n" next end - credential = ["\0#{user}\0#{password}"].pack('m0') - assert_equal(credential, comm.split[2]) - sock.puts "235 2.7.0 Authentication successful\r\n" + _, type, arg = comm.split + unless authtype.split.map(&:upcase).include? type.upcase + sock.puts "535 5.7.8 Error: authentication failed: no mechanism available\r\n" + next + end + # The account and password are fixed to "account" and "password". + result = case type + when 'PLAIN' + arg == 'AGFjY291bnQAcGFzc3dvcmQ=' + when 'LOGIN' + sock.puts '334 VXNlcm5hbWU6' + u = sock.gets.unpack1('m') + sock.puts '334 UGFzc3dvcmQ6' + p = sock.gets.unpack1('m') + u == 'account' && p == 'password' + when 'CRAM-MD5' + sock.puts "334 PDEyMzQ1Njc4OTAuMTIzNDVAc2VydmVybmFtZT4=\r\n" + r = sock.gets&.chomp + r == 'YWNjb3VudCAyYzBjMTgxZjkxOGU2ZGM5Mjg3Zjk3N2E1ODhiMzg1YQ==' + end + if result + sock.puts "235 2.7.0 Authentication successful\r\n" + else + sock.puts "535 5.7.8 Error: authentication failed: authentication failure\r\n" + end when "QUIT" sock.puts "221 2.0.0 Bye\r\n" sock.close