From 21412921c0717faf02f8275424c36bf0120d53a9 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 20 Jan 2023 23:52:57 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Add=20deprecation=20war?= =?UTF-8?q?nings=20to=20.new=20and=20#starttls=20[=F0=9F=9A=A7=20WIP]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `ssl` was renamed to `tls` in most places, with backwards compatible aliases. Using `ssl` does not print any deprecation warnings. Using both `tls` and `ssl` keywords raises an ArgumentError. * Preparing for a (backwards-incompatible) secure-by-default configuration, `Net::IMAP.default_tls` will determine the value for `tls` when no explicit port or tls setting is provided. Using port 143 will be insecure by default. Using port 993 will be secure by default. Providing no explicit port will use `Net::IMAP.default_tls` with the appropriate port. And providing any other unknown port will use `default_tls` with a warning. 🚧 TODO: should we use a different config var for default tls params when port is 993 and `tls` is unspecified? 🚧 TODO: should we use a different config var for choosing `tls` when `port` is non-standard vs choosing `port` and `tls` when neither are specified? 🚧 TODO: should we use a different var for `default_tls` be used to config params when port is 993 but tls is implicit? Another var? --- lib/net/imap.rb | 60 ++++++++++++++++++++--- lib/net/imap/deprecated_client_options.rb | 29 +++++++++-- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 50ab01d2..029ec7f3 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -704,6 +704,19 @@ class << self alias default_imap_port default_port alias default_imaps_port default_tls_port alias default_ssl_port default_tls_port + + # The default value for the +tls+ option of ::new, when +port+ is + # unspecified or non-standard. + # + # *Note*: A future release of Net::IMAP will set the default to +true+, as + # per RFC7525[https://tools.ietf.org/html/rfc7525], + # RFC7817[https://tools.ietf.org/html/rfc7817], and + # RFC8314[https://tools.ietf.org/html/rfc8314]. + # + # Set to +true+ for the secure default without warnings. Set to + # +false+ to globally silence warnings and use insecure defaults. + attr_accessor :default_tls + alias default_ssl default_tls end # Returns the initial greeting the server, an UntaggedResponse. @@ -746,16 +759,28 @@ class << self # Accepts the following options: # # [port] - # Port number. Defaults to 993 when +ssl+ is truthy, and 143 otherwise. + # Port number. Defaults to 143 when +tls+ is false, 993 when +tls+ is + # truthy. Based on ::default_tls when both +port+ and +tls+ are nil. # - # [ssl] + # [tls] # If +true+, the connection will use TLS with the default params set by # {OpenSSL::SSL::SSLContext#set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params]. - # If +ssl+ is a hash, it's passed to + # If +tls+ is a hash, it's passed to # {OpenSSL::SSL::SSLContext#set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params]; # the keys are names of attribute assignment methods on # SSLContext[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html]. # + # When port: 993, +tls+ defaults to +true+. + # When port: 143, +tls+ defaults to +false+. + # When port is unspecified or non-standard, +tls+ defaults to + # ::default_tls. When ::default_tls is also +nil+, a warning is printed + # and the connection does _not_ use TLS. + # + # When +nil+ or unassigned a default value is assigned: the default is + # +true+ if port: 993, +false+ if port: 143, and + # ::default_tls when +port+ is unspecified or non-standard. When + # ::default_tls is +nil+, a back + # # [open_timeout] # Seconds to wait until a connection is opened # [idle_response_timeout] @@ -816,15 +841,15 @@ class << self # [Net::IMAP::ByeResponseError] # Connected to the host successfully, but it immediately said goodbye. # - def initialize(host, port: nil, ssl: nil, + def initialize(host, port: nil, tls: nil, open_timeout: 30, idle_response_timeout: 5) super() # Config options @host = host - @port = port || (ssl ? SSL_PORT : PORT) + tls, @port = default_tls_and_port(tls, port) @open_timeout = Integer(open_timeout) @idle_response_timeout = Integer(idle_response_timeout) - @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(ssl) + @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(tls) # Basic Client State @utf8_strings = false @@ -939,7 +964,7 @@ def capabilities # servers will drop all AUTH= mechanisms from #capabilities after # the connection has authenticated. # - # imap = Net::IMAP.new(hostname, ssl: false) + # imap = Net::IMAP.new(hostname, tls: false) # imap.capabilities # => ["IMAP4REV1", "LOGINDISABLED"] # imap.auth_mechanisms # => [] # @@ -2394,6 +2419,27 @@ def remove_response_handler(handler) @@debug = false + def default_tls_and_port(tls, port) + if tls.nil? && port + tls = true if port == SSL_PORT || /\Aimaps\z/i === port + tls = false if port == PORT + elsif port.nil? && !tls.nil? + port = tls ? SSL_PORT : PORT + end + if tls.nil? && port.nil? + tls = self.class.default_tls.dup.freeze + port = tls ? SSL_PORT : PORT + if tls.nil? + warn "A future version of Net::IMAP.default_tls " \ + "will default to 'true', for secure connections by default. " \ + "Use 'Net::IMAP.new(host, tls: false)' or set " \ + "Net::IMAP.default_tls = false' to silence this warning." + end + end + tls &&= tls.respond_to?(:to_hash) ? tls.to_hash : {} + [tls, port] + end + def start_imap_connection @greeting = get_server_greeting @capabilities = capabilities_from_resp_code @greeting diff --git a/lib/net/imap/deprecated_client_options.rb b/lib/net/imap/deprecated_client_options.rb index d747d68b..341df4ef 100644 --- a/lib/net/imap/deprecated_client_options.rb +++ b/lib/net/imap/deprecated_client_options.rb @@ -5,9 +5,12 @@ class IMAP < Protocol # This module handles deprecated arguments to various Net::IMAP methods. module DeprecatedClientOptions + UNDEF = Module.new.freeze + private_constant :UNDEF # :call-seq: # Net::IMAP.new(host, **options) # standard keyword options + # Net::IMAP.new(host, ssl: nil, **options) # ssl => tls # Net::IMAP.new(host, options) # obsolete hash options # Net::IMAP.new(host, port) # obsolete port argument # Net::IMAP.new(host, port, usessl, certs = nil, verify = true) # deprecated SSL arguments @@ -19,6 +22,13 @@ module DeprecatedClientOptions # Using obsolete arguments does not a warning. Obsolete arguments will be # deprecated by a future release. # + # If +ssl+ is given, it is silently converted to the +tls+ keyword + # argument. Combining both +ssl+ and +tls+ raises an ArgumentError. Both + # of the following behave identically: + # + # Net::IMAP.new("imap.example.com", port: 993, ssl: {ca_path: "path/to/certs"}) + # Net::IMAP.new("imap.example.com", port: 993, tls: {ca_path: "path/to/certs"}) + # # If a second positional argument is given and it is a hash (or is # convertable via +#to_hash+), it is converted to keyword arguments. # @@ -71,6 +81,7 @@ module DeprecatedClientOptions # def initialize(host, port_or_options = nil, *deprecated, **options) if port_or_options.nil? && deprecated.empty? + translate_ssl_to_tls(options) super host, **options elsif options.any? # Net::IMAP.new(host, *__invalid__, **options) @@ -79,15 +90,17 @@ def initialize(host, port_or_options = nil, *deprecated, **options) # Net::IMAP.new(host, options, *__invalid__) raise ArgumentError, "Do not use deprecated SSL params with options hash" elsif port_or_options.respond_to?(:to_hash) - super host, **Hash.try_convert(port_or_options) + options = Hash.try_convert(port_or_options) + translate_ssl_to_tls(options) + super host, **options elsif deprecated.empty? super host, port: port_or_options elsif deprecated.shift warn "DEPRECATED: Call Net::IMAP.new with keyword options", uplevel: 1 - super host, port: port_or_options, ssl: create_ssl_params(*deprecated) + super host, port: port_or_options, tls: create_ssl_params(*deprecated) else warn "DEPRECATED: Call Net::IMAP.new with keyword options", uplevel: 1 - super host, port: port_or_options, ssl: false + super host, port: port_or_options, tls: false end end @@ -120,7 +133,17 @@ def starttls(*deprecated, **options) private + def translate_ssl_to_tls(options) + return unless options.key?(:ssl) + if options.key?(:tls) + raise ArgumentError, "conflicting :ssl and :tls keyword arguments" + end + options.merge!(tls: options.delete(:ssl)) + end + def create_ssl_params(certs = nil, verify = true) + certs = nil if certs == UNDEF + verify = true if verify == UNDEF params = {} if certs if File.file?(certs)