module SslDiagnostics

An SSL diagnotic tool. See mislav.uniqpath.com/2013/07/ruby-openssl/

Constants

VERSION

Public Class Methods

run() click to toggle source
# File lib/ssl_diagnostics.rb, line 7
def self.run
  # Usage: ruby doctor.rb [HOST=status.github.com[:PORT=443]]
  require 'rbconfig'
  require 'net/https'

  if ENV['URL'] =~ /^[^-]/
    host, port = ENV['URL'].split(':', 2)
  else
    host = 'www.warwickshire.gov.uk'
  end
  port ||= 443

  ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
  ruby_version = RUBY_VERSION
  if patch = RbConfig::CONFIG['PATCHLEVEL']
    ruby_version += "-p#{patch}"
  end
  puts "%s (%s)" % [ruby, ruby_version]

  openssl_dir = OpenSSL::X509::DEFAULT_CERT_AREA
  mac_openssl = '/System/Library/OpenSSL' == openssl_dir
  puts "%s: %s" % [OpenSSL::OPENSSL_VERSION, openssl_dir]
  [OpenSSL::X509::DEFAULT_CERT_DIR_ENV, OpenSSL::X509::DEFAULT_CERT_FILE_ENV].each do |key|
    puts "%s=%s" % [key, ENV[key].to_s.inspect]
  end

  ca_file = ENV[OpenSSL::X509::DEFAULT_CERT_FILE_ENV] || OpenSSL::X509::DEFAULT_CERT_FILE
  ca_path = (ENV[OpenSSL::X509::DEFAULT_CERT_DIR_ENV] || OpenSSL::X509::DEFAULT_CERT_DIR).chomp('/')

  puts "\nHEAD https://#{host}:#{port}"
  http = Net::HTTP.new(host, port)
  http.use_ssl = true

  # Explicitly setting cert_store like this is not needed in most cases but it
  # seems necessary in edge cases such as when using `verify_callback` in some
  # combination of Ruby + OpenSSL versions.
  http.cert_store = OpenSSL::X509::Store.new
  http.cert_store.set_default_paths

  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  failed_cert = failed_cert_reason = nil

  if mac_openssl
    warn "warning: will not be able show failed certificate info on OS X's OpenSSL"
    # This drives me absolutely nuts. It seems that on Rubies compiled against OS X's
    # system OpenSSL, the mere fact of defining a `verify_callback` makes the
    # cert verification fail for requests that would otherwise be successful.
  else
    http.verify_callback = lambda { |verify_ok, store_context|
      if !verify_ok
        failed_cert = store_context.current_cert
        failed_cert_reason = "%d: %s" % [ store_context.error, store_context.error_string ]
      end
      verify_ok
    }
  end

  user_agent = "net/http #{ruby_version}"
  req = Net::HTTP::Head.new('/', 'user-agent' => user_agent)

  begin
    res = http.start { http.request(req) }
    abort res.inspect if res.code.to_i >= 500
    puts "OK"
  rescue Errno::ECONNREFUSED
    puts "Error: connection refused"
    exit 1
  rescue OpenSSL::SSL::SSLError => e
    puts "#{e.class}: #{e.message}"

    if failed_cert
      puts "\nThe server presented a certificate that could not be verified:"
      puts "  subject: #{failed_cert.subject}"
      puts "  issuer: #{failed_cert.issuer}"
      puts "  error code %s" % failed_cert_reason
    end

    ca_file_missing = !File.exist?(ca_file) && !mac_openssl
    ca_path_empty = Dir["#{ca_path}/*"].empty?

    if ca_file_missing || ca_path_empty
      puts "\nPossible causes:"
      puts "  `%s' does not exist" % ca_file if ca_file_missing
      puts "  `%s/' is empty" % ca_path if ca_path_empty
    end

    exit 1
  end
end