diff options
-rw-r--r-- | lib/bundler/cli/doctor/ssl.rb | 76 | ||||
-rw-r--r-- | libexec/ssl_check.rb | 67 | ||||
-rw-r--r-- | spec/bundler/commands/ssl_spec.rb | 94 |
3 files changed, 170 insertions, 67 deletions
diff --git a/lib/bundler/cli/doctor/ssl.rb b/lib/bundler/cli/doctor/ssl.rb index f0d8a9b986..7ddca0c8a4 100644 --- a/lib/bundler/cli/doctor/ssl.rb +++ b/lib/bundler/cli/doctor/ssl.rb @@ -17,6 +17,8 @@ module Bundler output_ssl_environment bundler_success = bundler_connection_successful? rubygem_success = rubygem_connection_successful? + + return unless net_http_connection_successful? end private @@ -92,6 +94,29 @@ module Bundler false end + def net_http_connection_successful? + ::Gem::Net::HTTP.new(uri.host, uri.port).tap do |http| + http.use_ssl = true + http.min_version = tls_version + http.max_version = tls_version + http.verify_mode = verify_mode + end.start + + Bundler.ui.info("Ruby net/http: success") + + true + rescue StandardError => error + Bundler.ui.warn(<<~MSG) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to #{host}. + + #{Explanation.explain_net_http_error(error, host, tls_version)} + MSG + + false + end + module Explanation extend self @@ -107,6 +132,57 @@ module Bundler error.message end end + + def explain_net_http_error(error, host, tls_version) + case error.message + # Check for certificate errors + when /certificate verify failed/ + <<~MSG + #{show_ssl_certs} + Your Ruby can't connect to #{host} because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine #{host} servers. + MSG + # Check for TLS version errors + when /read server hello A/, /tlsv1 alert protocol version/ + if tls_version.to_s == "TLS1_3" + "Your Ruby can't connect to #{host} because #{tls_version} isn't supported yet.\n" + else + <<~MSG + Your Ruby can't connect to #{host} because your version of OpenSSL is too old. + You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL. + MSG + end + # OpenSSL doesn't support TLS version specified by argument + when /unknown SSL method/ + "Your Ruby can't connect because #{tls_version} isn't supported by your version of OpenSSL." + else + <<~MSG + Even worse, we're not sure why. + + Here's the full error information: + #{error.class}: #{error.message} + #{error.backtrace.join("\n ")} + + You might have more luck using Mislav's SSL doctor.rb script. You can get it here: + https://github.com/mislav/ssl-tools/blob/8b3dec4/doctor.rb + + Read more about the script and how to use it in this blog post: + https://mislav.net/2013/07/ruby-openssl/ + MSG + end + end + + private + + def show_ssl_certs + ssl_cert_file = ENV["SSL_CERT_FILE"] || OpenSSL::X509::DEFAULT_CERT_FILE + ssl_cert_dir = ENV["SSL_CERT_DIR"] || OpenSSL::X509::DEFAULT_CERT_DIR + + <<~MSG + Below affect only Ruby net/http connections: + SSL_CERT_FILE: #{File.exist?(ssl_cert_file) ? "exists #{ssl_cert_file}" : "is missing #{ssl_cert_file}"} + SSL_CERT_DIR: #{Dir.exist?(ssl_cert_dir) ? "exists #{ssl_cert_dir}" : "is missing #{ssl_cert_dir}"} + MSG + end end end end diff --git a/libexec/ssl_check.rb b/libexec/ssl_check.rb index 39f60c47a1..c29d3bfb0f 100644 --- a/libexec/ssl_check.rb +++ b/libexec/ssl_check.rb @@ -30,73 +30,6 @@ puts "Ruby: %s" % ruby_version puts "RubyGems: %s" % Gem::VERSION if defined?(Gem::VERSION) puts "Bundler: %s" % Bundler::VERSION if defined?(Bundler::VERSION) -def show_ssl_certs - puts "", "Below affect only Ruby net/http connections:" - puts - t = ENV['SSL_CERT_FILE'] || OpenSSL::X509::DEFAULT_CERT_FILE - ssl_file = File.exist?(t) ? "✅ exists #{t}" : "❌ is missing #{t}" - puts "SSL_CERT_FILE: %s" % ssl_file - - t = ENV['SSL_CERT_DIR'] || OpenSSL::X509::DEFAULT_CERT_DIR - ssl_dir = Dir.exist?(t) ? "✅ exists #{t}" : "❌ is missing #{t}" - puts "SSL_CERT_DIR: %s" % ssl_dir - puts -end - -begin - # Try to connect using HTTPS - Net::HTTP.new(uri.host, uri.port).tap do |http| - http.use_ssl = true - if tls_version - if http.respond_to? :min_version= - vers = tls_version.sub("v", "").to_sym - http.min_version = vers - http.max_version = vers - else - http.ssl_version = tls_version.to_sym - end - end - http.verify_mode = verify_mode - end.start - - puts "Ruby net/http: ✅ success" - puts -rescue => error - puts "Ruby net/http: ❌ failed" - puts - puts "Unfortunately, this Ruby can't connect to #{host}. 😡" - - case error.message - # Check for certificate errors - when /certificate verify failed/ - show_ssl_certs - puts "\nYour Ruby can't connect to #{host} because you are missing the certificate", - "files OpenSSL needs to verify you are connecting to the genuine #{host} servers.", "" - # Check for TLS version errors - when /read server hello A/, /tlsv1 alert protocol version/ - if tls_version == "TLSv1_3" - puts "\nYour Ruby can't connect to #{host} because #{tls_version} isn't supported yet.\n\n" - else - puts "\nYour Ruby can't connect to #{host} because your version of OpenSSL is too old.", - "You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL.", "" - end - # OpenSSL doesn't support TLS version specified by argument - when /unknown SSL method/ - puts "\nYour Ruby can't connect because #{tls_version} isn't supported by your version of OpenSSL.\n\n" - else - puts "\nEven worse, we're not sure why. 😕" - puts - puts "Here's the full error information:", - "#{error.class}: #{error.message}", - " #{error.backtrace.join("\n ")}" - puts - puts "You might have more luck using Mislav's SSL doctor.rb script. You can get it here:", - "https://github.com/mislav/ssl-tools/blob/8b3dec4/doctor.rb", - "Read more about the script and how to use it in this blog post:", - "https://mislav.net/2013/07/ruby-openssl/", "" - end - exit 1 -end guide_url = "http://ruby.to/ssl-check-failed" if bundler_status =~ /success/ && rubygems_status =~ /success/ diff --git a/spec/bundler/commands/ssl_spec.rb b/spec/bundler/commands/ssl_spec.rb index 1172bc9da7..4fc9db0016 100644 --- a/spec/bundler/commands/ssl_spec.rb +++ b/spec/bundler/commands/ssl_spec.rb @@ -67,6 +67,15 @@ RSpec.describe "bundle doctor ssl" do expected_err = <<~MSG Bundler: failed (certificate verification) RubyGems: failed (certificate verification) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to rubygems.org. + + Below affect only Ruby net/http connections: + SSL_CERT_FILE: exists #{OpenSSL::X509::DEFAULT_CERT_FILE} + SSL_CERT_DIR: exists #{OpenSSL::X509::DEFAULT_CERT_DIR} + + Your Ruby can't connect to rubygems.org because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine rubygems.org servers. MSG @@ -100,11 +109,54 @@ RSpec.describe "bundle doctor ssl" do expected_err = <<~MSG Bundler: failed (SSL/TLS protocol version mismatch) RubyGems: failed (SSL/TLS protocol version mismatch) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to rubygems.org. + + Your Ruby can't connect to rubygems.org because your version of OpenSSL is too old. + You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL. + + MSG + + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + + it "fails due to unsupported tls 1.3 version" do + net_http = Class.new(Artifice::Net::HTTP) do + def connect + raise OpenSSL::SSL::SSLError, "read server hello A" + end + end + + Artifice.replace_net_http(net_http) + Gem::Request::ConnectionPools.client = net_http + Gem::RemoteFetcher.fetcher.close_all + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + MSG + + expected_err = <<~MSG + Bundler: failed (SSL/TLS protocol version mismatch) + RubyGems: failed (SSL/TLS protocol version mismatch) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to rubygems.org. + + Your Ruby can't connect to rubygems.org because TLS1_3 isn't supported yet. MSG + subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3") expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr end + end context "when no diagnostic fails" do @@ -119,11 +171,53 @@ RSpec.describe "bundle doctor ssl" do Trying connections to https://rubygems.org: Bundler: success RubyGems: success + Ruby net/http: success MSG subject = Bundler::CLI::Doctor::SSL.new({}) expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr end + + it "uses the tls_version verify mode and host when given as option" do + net_http = Class.new(Artifice::Net::HTTP) do + class << self + attr_accessor :verify_mode, :min_version, :max_version + end + + def connect + self.class.verify_mode = verify_mode + self.class.min_version = min_version + self.class.max_version = max_version + + super + end + end + + net_http.endpoint = @dummy_endpoint + Artifice.replace_net_http(net_http) + Gem::Request::ConnectionPools.client = net_http + Gem::RemoteFetcher.fetcher.close_all + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://example.org: + Bundler: success + RubyGems: success + Ruby net/http: success + + MSG + + subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3", "verify-mode": :none, host: "example.org") + expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr + expect(net_http.verify_mode).to eq(0) + expect(net_http.min_version.to_s).to eq("TLS1_3") + expect(net_http.max_version.to_s).to eq("TLS1_3") + end end end |