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)
#
class SMTP < Protocol
VERSION = "0.1.0"
VERSION = "0.2.0"
Revision = %q$Revision$.split[1]
@ -191,8 +191,13 @@ module Net
alias default_ssl_port default_tls_port
end
def SMTP.default_ssl_context
OpenSSL::SSL::SSLContext.new
def SMTP.default_ssl_context(verify_peer=true)
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
#
@ -218,8 +223,9 @@ module Net
@error_occurred = false
@debug_output = nil
@tls = false
@starttls = false
@ssl_context = nil
@starttls = :auto
@ssl_context_tls = nil
@ssl_context_starttls = nil
end
# Provide human-readable stringification of class state.
@ -294,11 +300,11 @@ module Net
# Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
# this object. Must be called before the connection is established
# 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 ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls == :always
@tls = true
@ssl_context = context
@ssl_context_tls = context
end
alias enable_ssl enable_tls
@ -307,7 +313,7 @@ module Net
# connection is established to have any effect.
def disable_tls
@tls = false
@ssl_context = nil
@ssl_context_tls = nil
end
alias disable_ssl disable_tls
@ -331,27 +337,27 @@ module Net
# Enables SMTP/TLS (STARTTLS) for this 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 ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
@starttls = :always
@ssl_context = context
@ssl_context_starttls = context
end
# Enables SMTP/TLS (STARTTLS) for this object if server accepts.
# +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 ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
@starttls = :auto
@ssl_context = context
@ssl_context_starttls = context
end
# Disables SMTP/TLS (STARTTLS) for this object. Must be called
# before the connection is established to have any effect.
def disable_starttls
@starttls = false
@ssl_context = nil
@ssl_context_starttls = nil
end
# The address of the SMTP server to connect to.
@ -403,14 +409,14 @@ module Net
#
# :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| ... }
#
# Creates a new Net::SMTP object and connects to the server.
#
# 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
#
@ -440,6 +446,9 @@ module Net
# or other authentication token; and +authtype+ is the authentication
# type, one of :plain, :login, or :cram_md5. See the discussion of
# 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
#
@ -456,13 +465,14 @@ module Net
#
def SMTP.start(address, port = nil, *args, helo: nil,
user: nil, secret: nil, password: nil, authtype: nil,
tls_verify: true, tls_hostname: nil,
&block)
raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4
helo ||= args[0] || 'localhost'
user ||= args[1]
secret ||= password || args[2]
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
# +true+ if the SMTP session has been started.
@ -472,7 +482,7 @@ module Net
#
# :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| ... }
#
# 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
# :login, :plain, and :cram_md5. See the notes on SMTP Authentication
# 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
#
@ -526,12 +539,19 @@ module Net
# * IOError
#
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
helo ||= args[0] || 'localhost'
user ||= args[1]
secret ||= password || args[2]
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?
begin
do_start helo, user, secret, authtype
@ -568,16 +588,16 @@ module Net
tcp_socket(@address, @port)
end
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() }
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?
raise SMTPUnsupportedCommand,
"STARTTLS is not supported on this server"
end
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
do_helo helo_domain
end
@ -595,15 +615,15 @@ module Net
OpenSSL::SSL::SSLSocket.new socket, context
end
def tlsconnect(s)
def tlsconnect(s, context)
verified = false
s = ssl_socket(s, @ssl_context)
s = ssl_socket(s, context)
logging "TLS connection started"
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)
if @ssl_context.verify_mode && @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@address)
if context.verify_mode && context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@tls_hostname || @address)
end
verified = true
s

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

@ -137,7 +137,7 @@ module Net
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port)
smtp.enable_tls
smtp.open_timeout = 1
smtp.start do
smtp.start(tls_verify: false) do
end
ensure
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