summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/rubygems/commands/cert_command.rb34
-rw-r--r--lib/rubygems/config_file.rb12
-rw-r--r--lib/rubygems/installer.rb18
-rw-r--r--lib/rubygems/request_set.rb27
-rw-r--r--lib/rubygems/resolver/specification.rb14
-rw-r--r--lib/rubygems/security/signer.rb18
-rw-r--r--lib/rubygems/user_interaction.rb67
-rw-r--r--test/rubygems/test_gem_commands_cert_command.rb39
-rw-r--r--test/rubygems/test_gem_stream_ui.rb16
9 files changed, 165 insertions, 80 deletions
diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb
index aa26f340ff..3f74508074 100644
--- a/lib/rubygems/commands/cert_command.rb
+++ b/lib/rubygems/commands/cert_command.rb
@@ -14,15 +14,16 @@ class Gem::Commands::CertCommand < Gem::Command
super 'cert', 'Manage RubyGems certificates and signing settings',
:add => [], :remove => [], :list => [], :build => [], :sign => []
- OptionParser.accept OpenSSL::X509::Certificate do |certificate|
+ OptionParser.accept OpenSSL::X509::Certificate do |certificate_file|
begin
- OpenSSL::X509::Certificate.new File.read certificate
+ certificate = OpenSSL::X509::Certificate.new File.read certificate_file
rescue Errno::ENOENT
- raise OptionParser::InvalidArgument, "#{certificate}: does not exist"
+ raise OptionParser::InvalidArgument, "#{certificate_file}: does not exist"
rescue OpenSSL::X509::CertificateError
raise OptionParser::InvalidArgument,
- "#{certificate}: invalid X509 certificate"
+ "#{certificate_file}: invalid X509 certificate"
end
+ [certificate, certificate_file]
end
OptionParser.accept OpenSSL::PKey::RSA do |key_file|
@@ -42,7 +43,7 @@ class Gem::Commands::CertCommand < Gem::Command
end
add_option('-a', '--add CERT', OpenSSL::X509::Certificate,
- 'Add a trusted certificate.') do |cert, options|
+ 'Add a trusted certificate.') do |(cert, _), options|
options[:add] << cert
end
@@ -67,8 +68,9 @@ class Gem::Commands::CertCommand < Gem::Command
end
add_option('-C', '--certificate CERT', OpenSSL::X509::Certificate,
- 'Signing certificate for --sign') do |cert, options|
+ 'Signing certificate for --sign') do |(cert, cert_file), options|
options[:issuer_cert] = cert
+ options[:issuer_cert_file] = cert_file
end
add_option('-K', '--private-key KEY', OpenSSL::PKey::RSA,
@@ -89,6 +91,11 @@ class Gem::Commands::CertCommand < Gem::Command
'Days before the certificate expires') do |days, options|
options[:expiration_length_days] = days.to_i
end
+
+ add_option('-R', '--re-sign',
+ 'Re-signs the certificate from -C with the key from -K') do |resign, options|
+ options[:resign] = resign
+ end
end
def add_certificate certificate # :nodoc:
@@ -114,6 +121,14 @@ class Gem::Commands::CertCommand < Gem::Command
build email
end
+ if options[:resign]
+ re_sign_cert(
+ options[:issuer_cert],
+ options[:issuer_cert_file],
+ options[:key]
+ )
+ end
+
sign_certificates unless options[:sign].empty?
end
@@ -290,6 +305,13 @@ For further reading on signing gems see `ri Gem::Security`.
end
end
+ def re_sign_cert(cert, cert_path, private_key)
+ Gem::Security::Signer.re_sign_cert(cert, cert_path, private_key) do |expired_cert_path, new_expired_cert_path|
+ alert("Your certificate #{expired_cert_path} has been re-signed")
+ alert("Your expired certificate will be located at: #{new_expired_cert_path}")
+ end
+ end
+
private
def valid_email? email
diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb
index c0fe3d8964..9ef1236204 100644
--- a/lib/rubygems/config_file.rb
+++ b/lib/rubygems/config_file.rb
@@ -27,6 +27,7 @@ require 'rbconfig'
# +:backtrace+:: See #backtrace
# +:sources+:: Sets Gem::sources
# +:verbose+:: See #verbose
+# +:concurrent_downloads+:: See #concurrent_downloads
#
# gemrc files may exist in various locations and are read and merged in
# the following order:
@@ -43,6 +44,7 @@ class Gem::ConfigFile
DEFAULT_BULK_THRESHOLD = 1000
DEFAULT_VERBOSITY = true
DEFAULT_UPDATE_SOURCES = true
+ DEFAULT_CONCURRENT_DOWNLOADS = 8
##
# For Ruby packagers to set configuration defaults. Set in
@@ -105,6 +107,11 @@ class Gem::ConfigFile
attr_accessor :verbose
##
+ # Number of gem downloads that should be performed concurrently.
+
+ attr_accessor :concurrent_downloads
+
+ ##
# True if we want to update the SourceInfoCache every time, false otherwise
attr_accessor :update_sources
@@ -177,6 +184,7 @@ class Gem::ConfigFile
@bulk_threshold = DEFAULT_BULK_THRESHOLD
@verbose = DEFAULT_VERBOSITY
@update_sources = DEFAULT_UPDATE_SOURCES
+ @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS
operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
@@ -200,6 +208,7 @@ class Gem::ConfigFile
@path = @hash[:gempath] if @hash.key? :gempath
@update_sources = @hash[:update_sources] if @hash.key? :update_sources
@verbose = @hash[:verbose] if @hash.key? :verbose
+ @concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads
@disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server
@sources = @hash[:sources] if @hash.key? :sources
@@ -415,6 +424,9 @@ if you believe they were disclosed to a third party.
yaml_hash[:update_sources] = @hash.fetch(:update_sources, DEFAULT_UPDATE_SOURCES)
yaml_hash[:verbose] = @hash.fetch(:verbose, DEFAULT_VERBOSITY)
+ yaml_hash[:concurrent_downloads] =
+ @hash.fetch(:concurrent_downloads, DEFAULT_CONCURRENT_DOWNLOADS)
+
yaml_hash[:ssl_verify_mode] =
@hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode
diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb
index 8cb89e2e40..a3bb9b821e 100644
--- a/lib/rubygems/installer.rb
+++ b/lib/rubygems/installer.rb
@@ -771,30 +771,26 @@ TEXT
# return the stub script text used to launch the true Ruby script
def windows_stub_script(bindir, bin_file_name)
- rb_config = RbConfig::CONFIG
- rb_topdir = RbConfig::TOPDIR || File.dirname(rb_config["bindir"])
- # get ruby executable file name from RbConfig
- ruby_exe = "#{rb_config['RUBY_INSTALL_NAME']}#{rb_config['EXEEXT']}"
-
- if File.exist?(File.join bindir, ruby_exe)
+ rb_bindir = RbConfig::CONFIG["bindir"]
+ # All comparisons should be case insensitive
+ if bindir.downcase == rb_bindir.downcase
# stub & ruby.exe withing same folder. Portable
<<-TEXT
@ECHO OFF
@"%~dp0ruby.exe" "%~dpn0" %*
TEXT
- elsif bindir.downcase.start_with? rb_topdir.downcase
- # stub within ruby folder, but not standard bin. Portable
+ elsif bindir.downcase.start_with?((RbConfig::TOPDIR || File.dirname(rb_bindir)).downcase)
+ # stub within ruby folder, but not standard bin. Not portable
require 'pathname'
from = Pathname.new bindir
- to = Pathname.new "#{rb_topdir}/bin"
+ to = Pathname.new rb_bindir
rel = to.relative_path_from from
<<-TEXT
@ECHO OFF
@"%~dp0#{rel}/ruby.exe" "%~dpn0" %*
TEXT
else
- # outside ruby folder, maybe -user-install or bundler. Portable, but ruby
- # is dependent on PATH
+ # outside ruby folder, maybe -user-install or bundler. Portable
<<-TEXT
@ECHO OFF
@ruby.exe "%~dpn0" %*
diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb
index ed99d29295..c68e40ee9e 100644
--- a/lib/rubygems/request_set.rb
+++ b/lib/rubygems/request_set.rb
@@ -152,7 +152,34 @@ class Gem::RequestSet
@prerelease = options[:prerelease]
requests = []
+ download_queue = Queue.new
+ # Create a thread-safe list of gems to download
+ sorted_requests.each do |req|
+ download_queue << req
+ end
+
+ # Create N threads in a pool, have them download all the gems
+ threads = Gem.configuration.concurrent_downloads.times.map do
+ # When a thread pops this item, it knows to stop running. The symbol
+ # is queued here so that there will be one symbol per thread.
+ download_queue << :stop
+
+ Thread.new do
+ # The pop method will block waiting for items, so the only way
+ # to stop a thread from running is to provide a final item that
+ # means the thread should stop.
+ while req = download_queue.pop
+ break if req == :stop
+ req.spec.download options unless req.installed?
+ end
+ end
+ end
+
+ # Wait for all the downloads to finish before continuing
+ threads.each(&:value)
+
+ # Install requested gems after they have been downloaded
sorted_requests.each do |req|
if req.installed? then
req.spec.spec.build_extensions
diff --git a/lib/rubygems/resolver/specification.rb b/lib/rubygems/resolver/specification.rb
index 44989d39ae..530ea9994d 100644
--- a/lib/rubygems/resolver/specification.rb
+++ b/lib/rubygems/resolver/specification.rb
@@ -84,11 +84,7 @@ class Gem::Resolver::Specification
def install options = {}
require 'rubygems/installer'
- destination = options[:install_dir] || Gem.dir
-
- Gem.ensure_gem_subdirectories destination
-
- gem = source.download spec, destination
+ gem = download options
installer = Gem::Installer.at gem, options
@@ -97,6 +93,14 @@ class Gem::Resolver::Specification
@spec = installer.install
end
+ def download options
+ dir = options[:install_dir] || Gem.dir
+
+ Gem.ensure_gem_subdirectories dir
+
+ source.download spec, dir
+ end
+
##
# Returns true if this specification is installable on this platform.
diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb
index 1ee9c31be6..fc98f951bc 100644
--- a/lib/rubygems/security/signer.rb
+++ b/lib/rubygems/security/signer.rb
@@ -30,6 +30,24 @@ class Gem::Security::Signer
attr_reader :digest_name # :nodoc:
##
+ # Attemps to re-sign an expired cert with a given private key
+ def self.re_sign_cert(expired_cert, expired_cert_path, private_key)
+ return unless expired_cert.not_after < Time.now
+
+ expiry = expired_cert.not_after.strftime('%Y%m%d%H%M%S')
+ expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}"
+ new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file)
+
+ Gem::Security.write(expired_cert, new_expired_cert_path)
+
+ re_signed_cert = Gem::Security.re_sign(expired_cert, private_key)
+
+ Gem::Security.write(re_signed_cert, expired_cert_path)
+
+ yield(expired_cert_path, new_expired_cert_path) if block_given?
+ end
+
+ ##
# Creates a new signer with an RSA +key+ or path to a key, and a certificate
# +chain+ containing X509 certificates, encoding certificates or paths to
# certificates.
diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb
index 6a5aecf1b7..61e85549da 100644
--- a/lib/rubygems/user_interaction.rb
+++ b/lib/rubygems/user_interaction.rb
@@ -512,11 +512,10 @@ class Gem::StreamUI
# Return a download reporter object chosen from the current verbosity
def download_reporter(*args)
- case Gem.configuration.verbose
- when nil, false
+ if [nil, false].include?(Gem.configuration.verbose) || !@outs.tty?
SilentDownloadReporter.new(@outs, *args)
else
- VerboseDownloadReporter.new(@outs, *args)
+ ThreadedDownloadReporter.new(@outs, *args)
end
end
@@ -553,9 +552,11 @@ class Gem::StreamUI
end
##
- # A progress reporter that prints out messages about the current progress.
+ # A progress reporter that behaves nicely with threaded downloading.
- class VerboseDownloadReporter
+ class ThreadedDownloadReporter
+
+ MUTEX = Mutex.new
##
# The current file name being displayed
@@ -563,71 +564,43 @@ class Gem::StreamUI
attr_reader :file_name
##
- # The total bytes in the file
-
- attr_reader :total_bytes
-
- ##
- # The current progress (0 to 100)
-
- attr_reader :progress
-
- ##
- # Creates a new verbose download reporter that will display on
+ # Creates a new threaded download reporter that will display on
# +out_stream+. The other arguments are ignored.
def initialize(out_stream, *args)
@out = out_stream
- @progress = 0
end
##
- # Tells the download reporter that the +file_name+ is being fetched and
- # contains +total_bytes+.
+ # Tells the download reporter that the +file_name+ is being fetched.
+ # The other arguments are ignored.
- def fetch(file_name, total_bytes)
- @file_name = file_name
- @total_bytes = total_bytes.to_i
- @units = @total_bytes.zero? ? 'B' : '%'
-
- update_display(false)
+ def fetch(file_name, *args)
+ if @file_name.nil?
+ @file_name = file_name
+ locked_puts "Fetching #{@file_name}"
+ end
end
##
- # Updates the verbose download reporter for the given number of +bytes+.
+ # Updates the threaded download reporter for the given number of +bytes+.
def update(bytes)
- new_progress = if @units == 'B' then
- bytes
- else
- ((bytes.to_f * 100) / total_bytes.to_f).ceil
- end
-
- return if new_progress == @progress
-
- @progress = new_progress
- update_display
+ # Do nothing.
end
##
# Indicates the download is complete.
def done
- @progress = 100 if @units == '%'
- update_display(true, true)
+ # Do nothing.
end
private
-
- def update_display(show_progress = true, new_line = false) # :nodoc:
- return unless @out.tty?
-
- if show_progress then
- @out.print "\rFetching: %s (%3d%s)" % [@file_name, @progress, @units]
- else
- @out.print "Fetching: %s" % @file_name
+ def locked_puts(message)
+ MUTEX.synchronize do
+ @out.puts message
end
- @out.puts if new_line
end
end
end
diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb
index 3f67513c3f..3d9bbeb1df 100644
--- a/test/rubygems/test_gem_commands_cert_command.rb
+++ b/test/rubygems/test_gem_commands_cert_command.rb
@@ -9,14 +9,16 @@ end
class TestGemCommandsCertCommand < Gem::TestCase
ALTERNATE_CERT = load_cert 'alternate'
+ EXPIRED_PUBLIC_CERT = load_cert 'expired'
ALTERNATE_KEY_FILE = key_path 'alternate'
PRIVATE_KEY_FILE = key_path 'private'
PUBLIC_KEY_FILE = key_path 'public'
- ALTERNATE_CERT_FILE = cert_path 'alternate'
- CHILD_CERT_FILE = cert_path 'child'
- PUBLIC_CERT_FILE = cert_path 'public'
+ ALTERNATE_CERT_FILE = cert_path 'alternate'
+ CHILD_CERT_FILE = cert_path 'child'
+ PUBLIC_CERT_FILE = cert_path 'public'
+ EXPIRED_PUBLIC_CERT_FILE = cert_path 'expired'
def setup
super
@@ -582,6 +584,37 @@ ERROR: --private-key not specified and ~/.gem/gem-private_key.pem does not exis
assert_equal expected, @ui.error
end
+ def test_execute_re_sign
+ gem_path = File.join Gem.user_home, ".gem"
+ Dir.mkdir gem_path
+
+ path = File.join @tempdir, 'cert.pem'
+ Gem::Security.write EXPIRED_PUBLIC_CERT, path, 0600
+
+ assert_equal '/CN=nobody/DC=example', EXPIRED_PUBLIC_CERT.issuer.to_s
+
+ tmp_expired_cert_file = File.join(Dir.tmpdir, File.basename(EXPIRED_PUBLIC_CERT_FILE))
+ File.write(tmp_expired_cert_file, File.read(EXPIRED_PUBLIC_CERT_FILE))
+
+ @cmd.handle_options %W[
+ --private-key #{PRIVATE_KEY_FILE}
+ --certificate #{tmp_expired_cert_file}
+ --re-sign
+ ]
+
+ use_ui @ui do
+ @cmd.execute
+ end
+
+ expected_path = File.join(gem_path, "#{File.basename(tmp_expired_cert_file)}.expired")
+
+ assert_match(
+ /INFO: Your certificate #{tmp_expired_cert_file} has been re-signed\nINFO: Your expired certificate will be located at: #{expected_path}\.[0-9]+/,
+ @ui.output
+ )
+ assert_equal '', @ui.error
+ end
+
def test_handle_options
@cmd.handle_options %W[
--add #{PUBLIC_CERT_FILE}
diff --git a/test/rubygems/test_gem_stream_ui.rb b/test/rubygems/test_gem_stream_ui.rb
index 56ab1cee5b..5735bbc9a9 100644
--- a/test/rubygems/test_gem_stream_ui.rb
+++ b/test/rubygems/test_gem_stream_ui.rb
@@ -156,14 +156,14 @@ class TestGemStreamUI < Gem::TestCase
def test_download_reporter_anything
@cfg.verbose = 0
reporter = @sui.download_reporter
- assert_kind_of Gem::StreamUI::VerboseDownloadReporter, reporter
+ assert_kind_of Gem::StreamUI::ThreadedDownloadReporter, reporter
end
- def test_verbose_download_reporter
+ def test_threaded_download_reporter
@cfg.verbose = true
reporter = @sui.download_reporter
reporter.fetch 'a.gem', 1024
- assert_equal "Fetching: a.gem", @out.string
+ assert_equal "Fetching a.gem\n", @out.string
end
def test_verbose_download_reporter_progress
@@ -171,7 +171,7 @@ class TestGemStreamUI < Gem::TestCase
reporter = @sui.download_reporter
reporter.fetch 'a.gem', 1024
reporter.update 512
- assert_equal "Fetching: a.gem\rFetching: a.gem ( 50%)", @out.string
+ assert_equal "Fetching a.gem\n", @out.string
end
def test_verbose_download_reporter_progress_once
@@ -180,7 +180,7 @@ class TestGemStreamUI < Gem::TestCase
reporter.fetch 'a.gem', 1024
reporter.update 510
reporter.update 512
- assert_equal "Fetching: a.gem\rFetching: a.gem ( 50%)", @out.string
+ assert_equal "Fetching a.gem\n", @out.string
end
def test_verbose_download_reporter_progress_complete
@@ -189,7 +189,7 @@ class TestGemStreamUI < Gem::TestCase
reporter.fetch 'a.gem', 1024
reporter.update 510
reporter.done
- assert_equal "Fetching: a.gem\rFetching: a.gem ( 50%)\rFetching: a.gem (100%)\n", @out.string
+ assert_equal "Fetching a.gem\n", @out.string
end
def test_verbose_download_reporter_progress_nil_length
@@ -198,7 +198,7 @@ class TestGemStreamUI < Gem::TestCase
reporter.fetch 'a.gem', nil
reporter.update 1024
reporter.done
- assert_equal "Fetching: a.gem\rFetching: a.gem (1024B)\rFetching: a.gem (1024B)\n", @out.string
+ assert_equal "Fetching a.gem\n", @out.string
end
def test_verbose_download_reporter_progress_zero_length
@@ -207,7 +207,7 @@ class TestGemStreamUI < Gem::TestCase
reporter.fetch 'a.gem', 0
reporter.update 1024
reporter.done
- assert_equal "Fetching: a.gem\rFetching: a.gem (1024B)\rFetching: a.gem (1024B)\n", @out.string
+ assert_equal "Fetching a.gem\n", @out.string
end
def test_verbose_download_reporter_no_tty