ruby/lib/bundler/cli/doctor/ssl.rb

249 lines
7.7 KiB
Ruby

# frozen_string_literal: true
require "rubygems/remote_fetcher"
require "uri"
module Bundler
class CLI::Doctor::SSL
attr_reader :options
def initialize(options)
@options = options
end
def run
return unless openssl_installed?
output_ssl_environment
bundler_success = bundler_connection_successful?
rubygem_success = rubygem_connection_successful?
return unless net_http_connection_successful?
Explanation.summarize(bundler_success, rubygem_success, host)
end
private
def host
@options[:host] || "rubygems.org"
end
def tls_version
@options[:"tls-version"].then do |version|
"TLS#{version.sub(".", "_")}".to_sym if version
end
end
def verify_mode
mode = @options[:"verify-mode"] || :peer
@verify_mode ||= mode.then {|mod| OpenSSL::SSL.const_get("verify_#{mod}".upcase) }
end
def uri
@uri ||= URI("https://#{host}")
end
def openssl_installed?
require "openssl"
true
rescue LoadError
Bundler.ui.warn(<<~MSG)
Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to #{host}.
You'll need to recompile or reinstall Ruby with OpenSSL support and try again.
MSG
false
end
def output_ssl_environment
Bundler.ui.info(<<~MESSAGE)
Here's your OpenSSL environment:
OpenSSL: #{OpenSSL::VERSION}
Compiled with: #{OpenSSL::OPENSSL_VERSION}
Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
MESSAGE
end
def bundler_connection_successful?
Bundler.ui.info("\nTrying connections to #{uri}:\n")
bundler_uri = Gem::URI(uri.to_s)
Bundler::Fetcher.new(
Bundler::Source::Rubygems::Remote.new(bundler_uri)
).send(:connection).request(bundler_uri)
Bundler.ui.info("Bundler: success")
true
rescue StandardError => error
Bundler.ui.warn("Bundler: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})")
false
end
def rubygem_connection_successful?
Gem::RemoteFetcher.fetcher.fetch_path(uri)
Bundler.ui.info("RubyGems: success")
true
rescue StandardError => error
Bundler.ui.warn("RubyGems: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})")
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")
warn_on_unsupported_tls12
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
def warn_on_unsupported_tls12
ctx = OpenSSL::SSL::SSLContext.new
supported = true
if ctx.respond_to?(:min_version=)
begin
ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
rescue OpenSSL::SSL::SSLError, NameError
supported = false
end
else
supported = OpenSSL::SSL::SSLContext::METHODS.include?(:TLSv1_2) # rubocop:disable Naming/VariableNumber
end
Bundler.ui.warn(<<~EOM) unless supported
WARNING: Although your Ruby can connect to #{host} today, your OpenSSL is very old!
WARNING: You will need to upgrade OpenSSL to use #{host}.
EOM
end
module Explanation
extend self
def explain_bundler_or_rubygems_error(error)
case error.message
when /certificate verify failed/
"certificate verification"
when /read server hello A/
"SSL/TLS protocol version mismatch"
when /tlsv1 alert protocol version/
"requested TLS version is too old"
else
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
def summarize(bundler_success, rubygems_success, host)
guide_url = "http://ruby.to/ssl-check-failed"
message = if bundler_success && rubygems_success
<<~MSG
Hooray! This Ruby can connect to #{host}.
You are all set to use Bundler and RubyGems.
MSG
elsif !bundler_success && !rubygems_success
<<~MSG
For some reason, your Ruby installation can connect to #{host}, but neither RubyGems nor Bundler can.
The most likely fix is to manually upgrade RubyGems by following the instructions at #{guide_url}.
After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked.
MSG
elsif !bundler_success
<<~MSG
Although your Ruby installation and RubyGems can both connect to #{host}, Bundler is having trouble.
The most likely way to fix this is to upgrade Bundler by running `gem install bundler`.
Run this script again after doing that to make sure everything is all set.
If you're still having trouble, check out the troubleshooting guide at #{guide_url}.
MSG
else
<<~MSG
It looks like Ruby and Bundler can connect to #{host}, but RubyGems itself cannot.
You can likely solve this by manually downloading and installing a RubyGems update.
Visit #{guide_url} for instructions on how to manually upgrade RubyGems.
MSG
end
Bundler.ui.info("\n#{message}")
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