This commit is contained in:
Hiroshi SHIBATA 2020-11-17 14:17:45 +09:00
Родитель fcc88da5eb
Коммит cada6d85d0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: F9CF13417264FAC2
4 изменённых файлов: 297 добавлений и 28 удалений

Просмотреть файл

@ -168,7 +168,7 @@ module Net
# user: 'Your Account', secret: 'Your Password', authtype: :cram_md5) # user: 'Your Account', secret: 'Your Password', authtype: :cram_md5)
# #
class SMTP < Protocol class SMTP < Protocol
VERSION = "0.1.0" VERSION = "0.2.0"
Revision = %q$Revision$.split[1] Revision = %q$Revision$.split[1]
@ -191,8 +191,13 @@ module Net
alias default_ssl_port default_tls_port alias default_ssl_port default_tls_port
end end
def SMTP.default_ssl_context def SMTP.default_ssl_context(verify_peer=true)
OpenSSL::SSL::SSLContext.new context = OpenSSL::SSL::SSLContext.new
context.verify_mode = verify_peer ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
store = OpenSSL::X509::Store.new
store.set_default_paths
context.cert_store = store
context
end end
# #
@ -218,8 +223,9 @@ module Net
@error_occurred = false @error_occurred = false
@debug_output = nil @debug_output = nil
@tls = false @tls = false
@starttls = false @starttls = :auto
@ssl_context = nil @ssl_context_tls = nil
@ssl_context_starttls = nil
end end
# Provide human-readable stringification of class state. # Provide human-readable stringification of class state.
@ -294,11 +300,11 @@ module Net
# Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
# this object. Must be called before the connection is established # this object. Must be called before the connection is established
# to have any effect. +context+ is a OpenSSL::SSL::SSLContext object. # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object.
def enable_tls(context = SMTP.default_ssl_context) def enable_tls(context = nil)
raise 'openssl library not installed' unless defined?(OpenSSL) raise 'openssl library not installed' unless defined?(OpenSSL)
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls == :always
@tls = true @tls = true
@ssl_context = context @ssl_context_tls = context
end end
alias enable_ssl enable_tls alias enable_ssl enable_tls
@ -307,7 +313,7 @@ module Net
# connection is established to have any effect. # connection is established to have any effect.
def disable_tls def disable_tls
@tls = false @tls = false
@ssl_context = nil @ssl_context_tls = nil
end end
alias disable_ssl disable_tls alias disable_ssl disable_tls
@ -331,27 +337,27 @@ module Net
# Enables SMTP/TLS (STARTTLS) for this object. # Enables SMTP/TLS (STARTTLS) for this object.
# +context+ is a OpenSSL::SSL::SSLContext object. # +context+ is a OpenSSL::SSL::SSLContext object.
def enable_starttls(context = SMTP.default_ssl_context) def enable_starttls(context = nil)
raise 'openssl library not installed' unless defined?(OpenSSL) raise 'openssl library not installed' unless defined?(OpenSSL)
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
@starttls = :always @starttls = :always
@ssl_context = context @ssl_context_starttls = context
end end
# Enables SMTP/TLS (STARTTLS) for this object if server accepts. # Enables SMTP/TLS (STARTTLS) for this object if server accepts.
# +context+ is a OpenSSL::SSL::SSLContext object. # +context+ is a OpenSSL::SSL::SSLContext object.
def enable_starttls_auto(context = SMTP.default_ssl_context) def enable_starttls_auto(context = nil)
raise 'openssl library not installed' unless defined?(OpenSSL) raise 'openssl library not installed' unless defined?(OpenSSL)
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
@starttls = :auto @starttls = :auto
@ssl_context = context @ssl_context_starttls = context
end end
# Disables SMTP/TLS (STARTTLS) for this object. Must be called # Disables SMTP/TLS (STARTTLS) for this object. Must be called
# before the connection is established to have any effect. # before the connection is established to have any effect.
def disable_starttls def disable_starttls
@starttls = false @starttls = false
@ssl_context = nil @ssl_context_starttls = nil
end end
# The address of the SMTP server to connect to. # The address of the SMTP server to connect to.
@ -403,14 +409,14 @@ module Net
# #
# :call-seq: # :call-seq:
# start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... } # start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil) { |smtp| ... }
# start(address, port = nil, helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } # start(address, port = nil, helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
# #
# Creates a new Net::SMTP object and connects to the server. # Creates a new Net::SMTP object and connects to the server.
# #
# This method is equivalent to: # This method is equivalent to:
# #
# Net::SMTP.new(address, port).start(helo: helo_domain, user: account, secret: password, authtype: authtype) # Net::SMTP.new(address, port).start(helo: helo_domain, user: account, secret: password, authtype: authtype, tls_verify: flag, tls_hostname: hostname)
# #
# === Example # === Example
# #
@ -440,6 +446,9 @@ module Net
# or other authentication token; and +authtype+ is the authentication # or other authentication token; and +authtype+ is the authentication
# type, one of :plain, :login, or :cram_md5. See the discussion of # type, one of :plain, :login, or :cram_md5. See the discussion of
# SMTP Authentication in the overview notes. # SMTP Authentication in the overview notes.
# If +tls_verify+ is true, verify the server's certificate. The default is true.
# If the hostname in the server certificate is different from +address+,
# it can be specified with +tls_hostname+.
# #
# === Errors # === Errors
# #
@ -456,13 +465,14 @@ module Net
# #
def SMTP.start(address, port = nil, *args, helo: nil, def SMTP.start(address, port = nil, *args, helo: nil,
user: nil, secret: nil, password: nil, authtype: nil, user: nil, secret: nil, password: nil, authtype: nil,
tls_verify: true, tls_hostname: nil,
&block) &block)
raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4 raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4
helo ||= args[0] || 'localhost' helo ||= args[0] || 'localhost'
user ||= args[1] user ||= args[1]
secret ||= password || args[2] secret ||= password || args[2]
authtype ||= args[3] authtype ||= args[3]
new(address, port).start(helo: helo, user: user, secret: secret, authtype: authtype, &block) new(address, port).start(helo: helo, user: user, secret: secret, authtype: authtype, tls_verify: tls_verify, tls_hostname: tls_hostname, &block)
end end
# +true+ if the SMTP session has been started. # +true+ if the SMTP session has been started.
@ -472,7 +482,7 @@ module Net
# #
# :call-seq: # :call-seq:
# start(helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... } # start(helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil) { |smtp| ... }
# start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } # start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
# #
# Opens a TCP connection and starts the SMTP session. # Opens a TCP connection and starts the SMTP session.
@ -487,6 +497,9 @@ module Net
# the type of authentication to attempt; it must be one of # the type of authentication to attempt; it must be one of
# :login, :plain, and :cram_md5. See the notes on SMTP Authentication # :login, :plain, and :cram_md5. See the notes on SMTP Authentication
# in the overview. # in the overview.
# If +tls_verify+ is true, verify the server's certificate. The default is true.
# If the hostname in the server certificate is different from +address+,
# it can be specified with +tls_hostname+.
# #
# === Block Usage # === Block Usage
# #
@ -526,12 +539,19 @@ module Net
# * IOError # * IOError
# #
def start(*args, helo: nil, def start(*args, helo: nil,
user: nil, secret: nil, password: nil, authtype: nil) user: nil, secret: nil, password: nil, authtype: nil, tls_verify: true, tls_hostname: nil)
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..4)" if args.size > 4 raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..4)" if args.size > 4
helo ||= args[0] || 'localhost' helo ||= args[0] || 'localhost'
user ||= args[1] user ||= args[1]
secret ||= password || args[2] secret ||= password || args[2]
authtype ||= args[3] authtype ||= args[3]
if @tls && @ssl_context_tls.nil?
@ssl_context_tls = SMTP.default_ssl_context(tls_verify)
end
if @starttls && @ssl_context_starttls.nil?
@ssl_context_starttls = SMTP.default_ssl_context(tls_verify)
end
@tls_hostname = tls_hostname
if block_given? if block_given?
begin begin
do_start helo, user, secret, authtype do_start helo, user, secret, authtype
@ -568,16 +588,16 @@ module Net
tcp_socket(@address, @port) tcp_socket(@address, @port)
end end
logging "Connection opened: #{@address}:#{@port}" logging "Connection opened: #{@address}:#{@port}"
@socket = new_internet_message_io(tls? ? tlsconnect(s) : s) @socket = new_internet_message_io(tls? ? tlsconnect(s, @ssl_context_tls) : s)
check_response critical { recv_response() } check_response critical { recv_response() }
do_helo helo_domain do_helo helo_domain
if starttls_always? or (capable_starttls? and starttls_auto?) if ! tls? and (starttls_always? or (capable_starttls? and starttls_auto?))
unless capable_starttls? unless capable_starttls?
raise SMTPUnsupportedCommand, raise SMTPUnsupportedCommand,
"STARTTLS is not supported on this server" "STARTTLS is not supported on this server"
end end
starttls starttls
@socket = new_internet_message_io(tlsconnect(s)) @socket = new_internet_message_io(tlsconnect(s, @ssl_context_starttls))
# helo response may be different after STARTTLS # helo response may be different after STARTTLS
do_helo helo_domain do_helo helo_domain
end end
@ -595,15 +615,15 @@ module Net
OpenSSL::SSL::SSLSocket.new socket, context OpenSSL::SSL::SSLSocket.new socket, context
end end
def tlsconnect(s) def tlsconnect(s, context)
verified = false verified = false
s = ssl_socket(s, @ssl_context) s = ssl_socket(s, context)
logging "TLS connection started" logging "TLS connection started"
s.sync_close = true s.sync_close = true
s.hostname = @address if s.respond_to? :hostname= s.hostname = @tls_hostname || @address if s.respond_to? :hostname=
ssl_socket_connect(s, @open_timeout) ssl_socket_connect(s, @open_timeout)
if @ssl_context.verify_mode && @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE if context.verify_mode && context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@address) s.post_connection_check(@tls_hostname || @address)
end end
verified = true verified = true
s s

Просмотреть файл

@ -137,7 +137,7 @@ module Net
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port) smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port)
smtp.enable_tls smtp.enable_tls
smtp.open_timeout = 1 smtp.open_timeout = 1
smtp.start do smtp.start(tls_verify: false) do
end end
ensure ensure
sock.close if sock sock.close if sock

Просмотреть файл

@ -0,0 +1,128 @@
require 'net/smtp'
require 'test/unit'
module Net
class TestSSLContext < Test::Unit::TestCase
class MySMTP < SMTP
attr_reader :__ssl_context, :__tls_hostname
def initialize(socket)
@fake_socket = socket
super("smtp.example.com")
end
def tcp_socket(*)
@fake_socket
end
def ssl_socket_connect(*)
end
def tlsconnect(*)
super
@fake_socket
end
def ssl_socket(socket, context)
@__ssl_context = context
s = super
hostname = @__tls_hostname = ''
s.define_singleton_method(:post_connection_check){ |name| hostname.replace(name) }
s
end
end
def teardown
@server_thread&.exit
@server_socket&.close
@client_socket&.close
end
def start_smtpd(starttls)
@server_socket, @client_socket = UNIXSocket.pair
@starttls_executed = false
@server_thread = Thread.new(@server_socket) do |s|
s.puts "220 fakeserver\r\n"
while cmd = s.gets&.chomp
case cmd
when /\AEHLO /
s.puts "250-fakeserver\r\n"
s.puts "250-STARTTLS\r\n" if starttls
s.puts "250 8BITMIME\r\n"
when /\ASTARTTLS/
@starttls_executed = true
s.puts "220 2.0.0 Ready to start TLS\r\n"
else
raise "unsupported command: #{cmd}"
end
end
end
@client_socket
end
def test_default
smtp = MySMTP.new(start_smtpd(true))
smtp.start
assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode)
end
def test_enable_tls
smtp = MySMTP.new(start_smtpd(true))
context = OpenSSL::SSL::SSLContext.new
smtp.enable_tls(context)
smtp.start
assert_equal(context, smtp.__ssl_context)
end
def test_enable_tls_before_disable_starttls
smtp = MySMTP.new(start_smtpd(true))
context = OpenSSL::SSL::SSLContext.new
smtp.enable_tls(context)
smtp.disable_starttls
smtp.start
assert_equal(context, smtp.__ssl_context)
end
def test_enable_starttls
smtp = MySMTP.new(start_smtpd(true))
context = OpenSSL::SSL::SSLContext.new
smtp.enable_starttls(context)
smtp.start
assert_equal(context, smtp.__ssl_context)
end
def test_enable_starttls_before_disable_tls
smtp = MySMTP.new(start_smtpd(true))
context = OpenSSL::SSL::SSLContext.new
smtp.enable_starttls(context)
smtp.disable_tls
smtp.start
assert_equal(context, smtp.__ssl_context)
end
def test_start_with_tls_verify_true
smtp = MySMTP.new(start_smtpd(true))
smtp.start(tls_verify: true)
assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode)
end
def test_start_with_tls_verify_false
smtp = MySMTP.new(start_smtpd(true))
smtp.start(tls_verify: false)
assert_equal(OpenSSL::SSL::VERIFY_NONE, smtp.__ssl_context.verify_mode)
end
def test_start_with_tls_hostname
smtp = MySMTP.new(start_smtpd(true))
smtp.start(tls_hostname: "localhost")
assert_equal("localhost", smtp.__tls_hostname)
end
def test_start_without_tls_hostname
smtp = MySMTP.new(start_smtpd(true))
smtp.start
assert_equal("smtp.example.com", smtp.__tls_hostname)
end
end
end

Просмотреть файл

@ -0,0 +1,121 @@
require 'net/smtp'
require 'test/unit'
module Net
class TestStarttls < Test::Unit::TestCase
class MySMTP < SMTP
def initialize(socket)
@fake_socket = socket
super("smtp.example.com")
end
def tcp_socket(*)
@fake_socket
end
def tlsconnect(*)
@fake_socket
end
end
def teardown
@server_thread&.exit
@server_socket&.close
@client_socket&.close
end
def start_smtpd(starttls)
@server_socket, @client_socket = UNIXSocket.pair
@starttls_executed = false
@server_thread = Thread.new(@server_socket) do |s|
s.puts "220 fakeserver\r\n"
while cmd = s.gets&.chomp
case cmd
when /\AEHLO /
s.puts "250-fakeserver\r\n"
s.puts "250-STARTTLS\r\n" if starttls
s.puts "250 8BITMIME\r\n"
when /\ASTARTTLS/
@starttls_executed = true
s.puts "220 2.0.0 Ready to start TLS\r\n"
else
raise "unsupported command: #{cmd}"
end
end
end
@client_socket
end
def test_default_with_starttls_capable
smtp = MySMTP.new(start_smtpd(true))
smtp.start
assert(@starttls_executed)
end
def test_default_without_starttls_capable
smtp = MySMTP.new(start_smtpd(false))
smtp.start
assert(!@starttls_executed)
end
def test_enable_starttls_with_starttls_capable
smtp = MySMTP.new(start_smtpd(true))
smtp.enable_starttls
smtp.start
assert(@starttls_executed)
end
def test_enable_starttls_without_starttls_capable
smtp = MySMTP.new(start_smtpd(false))
smtp.enable_starttls
err = assert_raise(Net::SMTPUnsupportedCommand) { smtp.start }
assert_equal("STARTTLS is not supported on this server", err.message)
end
def test_enable_starttls_auto_with_starttls_capable
smtp = MySMTP.new(start_smtpd(true))
smtp.enable_starttls_auto
smtp.start
assert(@starttls_executed)
end
def test_tls_with_starttls_capable
smtp = MySMTP.new(start_smtpd(true))
smtp.enable_tls
smtp.start
assert(!@starttls_executed)
end
def test_tls_without_starttls_capable
smtp = MySMTP.new(start_smtpd(false))
smtp.enable_tls
end
def test_disable_starttls
smtp = MySMTP.new(start_smtpd(true))
smtp.disable_starttls
smtp.start
assert(!@starttls_executed)
end
def test_enable_tls_and_enable_starttls
smtp = MySMTP.new(start_smtpd(true))
smtp.enable_tls
err = assert_raise(ArgumentError) { smtp.enable_starttls }
assert_equal("SMTPS and STARTTLS is exclusive", err.message)
end
def test_enable_tls_and_enable_starttls_auto
smtp = MySMTP.new(start_smtpd(true))
smtp.enable_tls
err = assert_raise(ArgumentError) { smtp.enable_starttls_auto }
assert_equal("SMTPS and STARTTLS is exclusive", err.message)
end
def test_enable_starttls_and_enable_starttls_auto
smtp = MySMTP.new(start_smtpd(true))
smtp.enable_starttls
assert_nothing_raised { smtp.enable_starttls_auto }
end
end
end