From e27381d289cbdbdca434bcc957c2cd1beab1c82c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 11 Nov 2021 11:00:30 +0900 Subject: Merge RubyGems 3.2.30 and Bundler 2.2.30 --- lib/bundler.rb | 3 +- lib/bundler/cli/info.rb | 15 ++- lib/bundler/cli/issue.rb | 7 +- lib/bundler/compact_index_client.rb | 4 +- lib/bundler/definition.rb | 16 +-- lib/bundler/digest.rb | 71 +++++++++++++ lib/bundler/errors.rb | 20 +++- lib/bundler/fetcher.rb | 3 +- lib/bundler/friendly_errors.rb | 35 +------ lib/bundler/gem_helper.rb | 21 +--- lib/bundler/rubygems_ext.rb | 4 + lib/bundler/rubygems_gem_installer.rb | 24 ++++- lib/bundler/rubygems_integration.rb | 35 +++++-- lib/bundler/runtime.rb | 2 +- lib/bundler/source/git.rb | 4 +- lib/bundler/source/rubygems.rb | 106 +++++++------------ .../vendor/connection_pool/lib/connection_pool.rb | 113 +++++++-------------- .../lib/connection_pool/monotonic_time.rb | 66 ------------ .../lib/connection_pool/timed_stack.rb | 40 ++++---- .../connection_pool/lib/connection_pool/version.rb | 2 +- .../connection_pool/lib/connection_pool/wrapper.rb | 57 +++++++++++ lib/bundler/version.rb | 2 +- lib/bundler/worker.rb | 4 +- lib/rubygems.rb | 4 +- lib/rubygems/commands/cert_command.rb | 19 ++-- lib/rubygems/commands/setup_command.rb | 2 +- lib/rubygems/core_ext/tcpsocket_init.rb | 4 +- lib/rubygems/installer.rb | 2 +- lib/rubygems/package.rb | 58 +++++------ lib/rubygems/query_utils.rb | 8 +- lib/rubygems/remote_fetcher.rb | 4 +- lib/rubygems/request.rb | 2 + lib/rubygems/request/connection_pools.rb | 2 +- lib/rubygems/request/http_pool.rb | 2 +- lib/rubygems/request_set.rb | 2 +- lib/rubygems/s3_uri_signer.rb | 9 +- lib/rubygems/security.rb | 64 +++++++++--- lib/rubygems/security/policy.rb | 8 +- lib/rubygems/security/signer.rb | 7 +- lib/rubygems/source.rb | 2 +- lib/rubygems/source/git.rb | 4 +- lib/rubygems/specification.rb | 2 +- lib/rubygems/user_interaction.rb | 2 +- lib/rubygems/util.rb | 2 +- 44 files changed, 459 insertions(+), 404 deletions(-) create mode 100644 lib/bundler/digest.rb delete mode 100644 lib/bundler/vendor/connection_pool/lib/connection_pool/monotonic_time.rb create mode 100644 lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb (limited to 'lib') diff --git a/lib/bundler.rb b/lib/bundler.rb index a7676cd497..1ab87ac299 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -37,12 +37,13 @@ module Bundler environment_preserver = EnvironmentPreserver.from_env ORIGINAL_ENV = environment_preserver.restore environment_preserver.replace_with_backup - SUDO_MUTEX = Mutex.new + SUDO_MUTEX = Thread::Mutex.new autoload :Definition, File.expand_path("bundler/definition", __dir__) autoload :Dependency, File.expand_path("bundler/dependency", __dir__) autoload :DepProxy, File.expand_path("bundler/dep_proxy", __dir__) autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__) + autoload :Digest, File.expand_path("bundler/digest", __dir__) autoload :Dsl, File.expand_path("bundler/dsl", __dir__) autoload :EndpointSpecification, File.expand_path("bundler/endpoint_specification", __dir__) autoload :Env, File.expand_path("bundler/env", __dir__) diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index 3111b64a33..3afed89ceb 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -40,12 +40,13 @@ module Bundler end def print_gem_path(spec) - if spec.name == "bundler" + name = spec.name + if name == "bundler" path = File.expand_path("../../../..", __FILE__) else path = spec.full_gem_path - unless File.directory?(path) - return Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at: #{path}" + if spec.deleted_gem? + return Bundler.ui.warn "The gem #{name} has been deleted. It was installed at: #{path}" end end @@ -54,8 +55,9 @@ module Bundler def print_gem_info(spec) metadata = spec.metadata + name = spec.name gem_info = String.new - gem_info << " * #{spec.name} (#{spec.version}#{spec.git_version})\n" + gem_info << " * #{name} (#{spec.version}#{spec.git_version})\n" gem_info << "\tSummary: #{spec.summary}\n" if spec.summary gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage gem_info << "\tDocumentation: #{metadata["documentation_uri"]}\n" if metadata.key?("documentation_uri") @@ -67,6 +69,11 @@ module Bundler gem_info << "\tMailing List: #{metadata["mailing_list_uri"]}\n" if metadata.key?("mailing_list_uri") gem_info << "\tPath: #{spec.full_gem_path}\n" gem_info << "\tDefault Gem: yes" if spec.respond_to?(:default_gem?) && spec.default_gem? + + if spec.deleted_gem? + return Bundler.ui.warn "The gem #{name} has been deleted. Gemspec information is still available though:\n#{gem_info}" + end + Bundler.ui.info gem_info end end diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb index f4cd5ac4df..b891ecb1d2 100644 --- a/lib/bundler/cli/issue.rb +++ b/lib/bundler/cli/issue.rb @@ -20,9 +20,10 @@ module Bundler Hopefully the troubleshooting steps above resolved your problem! If things still aren't working the way you expect them to, please let us know so - that we can diagnose and help fix the problem you're having. Please - view the Filing Issues guide for more information: - https://github.com/rubygems/rubygems/blob/master/bundler/doc/contributing/ISSUES.md + that we can diagnose and help fix the problem you're having, by filling + in the new issue form located at + https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md, + and copy and pasting the information below. EOS diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb index cf67f0e7a0..d5dbeb3b10 100644 --- a/lib/bundler/compact_index_client.rb +++ b/lib/bundler/compact_index_client.rb @@ -5,7 +5,7 @@ require "set" module Bundler class CompactIndexClient - DEBUG_MUTEX = Mutex.new + DEBUG_MUTEX = Thread::Mutex.new def self.debug return unless ENV["DEBUG_COMPACT_INDEX"] DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") } @@ -25,7 +25,7 @@ module Bundler @endpoints = Set.new @info_checksums_by_name = {} @parsed_checksums = false - @mutex = Mutex.new + @mutex = Thread::Mutex.new end def execution_mode=(block) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 4a27c438f5..9736d6551b 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -73,7 +73,6 @@ module Bundler @lockfile_contents = String.new @locked_bundler_version = nil @locked_ruby_version = nil - @locked_specs_incomplete_for_platform = false @new_platform = nil if lockfile && File.exist?(lockfile) @@ -139,6 +138,8 @@ module Bundler @dependency_changes = converge_dependencies @local_changes = converge_locals + @locked_specs_incomplete_for_platform = !@locked_specs.for(expand_dependencies(requested_dependencies & locked_dependencies), true, true) + @requires = compute_requires end @@ -228,6 +229,10 @@ module Bundler end end + def locked_dependencies + @locked_deps.values + end + def specs_for(groups) groups = requested_groups if groups.empty? deps = dependencies_for(groups) @@ -367,8 +372,8 @@ module Bundler new_sources = gemfile_sources - @locked_sources deleted_sources = @locked_sources - gemfile_sources - new_deps = @dependencies - @locked_deps.values - deleted_deps = @locked_deps.values - @dependencies + new_deps = @dependencies - locked_dependencies + deleted_deps = locked_dependencies - @dependencies # Check if it is possible that the source is only changed thing if (new_deps.empty? && deleted_deps.empty?) && (!new_sources.empty? && !deleted_sources.empty?) @@ -560,7 +565,7 @@ module Bundler def dependencies_for_source_changed?(source, locked_source = source) deps_for_source = @dependencies.select {|s| s.source == source } - locked_deps_for_source = @locked_deps.values.select {|dep| dep.source == locked_source } + locked_deps_for_source = locked_dependencies.select {|dep| dep.source == locked_source } deps_for_source.uniq.sort != locked_deps_for_source.sort end @@ -644,7 +649,7 @@ module Bundler def converge_dependencies frozen = Bundler.frozen_bundle? - (@dependencies + @locked_deps.values).each do |dep| + (@dependencies + locked_dependencies).each do |dep| locked_source = @locked_deps[dep.name] # This is to make sure that if bundler is installing in deployment mode and # after locked_source and sources don't match, we still use locked_source. @@ -751,7 +756,6 @@ module Bundler end resolve = SpecSet.new(converged) - @locked_specs_incomplete_for_platform = !resolve.for(expand_dependencies(requested_dependencies & deps), true, true) resolve = SpecSet.new(resolve.for(expand_dependencies(deps, true), false, false).reject{|s| @unlock[:gems].include?(s.name) }) diff = nil diff --git a/lib/bundler/digest.rb b/lib/bundler/digest.rb new file mode 100644 index 0000000000..759f609416 --- /dev/null +++ b/lib/bundler/digest.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# This code was extracted from https://github.com/Solistra/ruby-digest which is under public domain +module Bundler + module Digest + # The initial constant values for the 32-bit constant words A, B, C, D, and + # E, respectively. + SHA1_WORDS = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0].freeze + + # The 8-bit field used for bitwise `AND` masking. Defaults to `0xFFFFFFFF`. + SHA1_MASK = 0xFFFFFFFF + + class << self + def sha1(string) + unless string.is_a?(String) + raise TypeError, "can't convert #{string.class.inspect} into String" + end + + buffer = string.b + + words = SHA1_WORDS.dup + generate_split_buffer(buffer) do |chunk| + w = [] + chunk.each_slice(4) do |a, b, c, d| + w << (((a << 8 | b) << 8 | c) << 8 | d) + end + a, b, c, d, e = *words + (16..79).each do |i| + w[i] = SHA1_MASK & rotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1) + end + 0.upto(79) do |i| + case i + when 0..19 + f = ((b & c) | (~b & d)) + k = 0x5A827999 + when 20..39 + f = (b ^ c ^ d) + k = 0x6ED9EBA1 + when 40..59 + f = ((b & c) | (b & d) | (c & d)) + k = 0x8F1BBCDC + when 60..79 + f = (b ^ c ^ d) + k = 0xCA62C1D6 + end + t = SHA1_MASK & (SHA1_MASK & rotate(a, 5) + f + e + k + w[i]) + a, b, c, d, e = t, a, SHA1_MASK & rotate(b, 30), c, d # rubocop:disable Style/ParallelAssignment + end + mutated = [a, b, c, d, e] + words.map!.with_index {|word, index| SHA1_MASK & (word + mutated[index]) } + end + + words.pack("N*").unpack("H*").first + end + + private + + def generate_split_buffer(string, &block) + size = string.bytesize * 8 + buffer = string.bytes << 128 + buffer << 0 while buffer.size % 64 != 56 + buffer.concat([size].pack("Q>").bytes) + buffer.each_slice(64, &block) + end + + def rotate(value, spaces) + value << spaces | value >> (32 - spaces) + end + end + end +end diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index 565aaeeb9d..9ad7460e58 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -75,10 +75,26 @@ module Bundler end end + def permission_type + case @permission_type + when :create + "executable permissions for all parent directories and write permissions for `#{parent_folder}`" + when :delete + permissions = "executable permissions for all parent directories and write permissions for `#{parent_folder}`" + permissions += ", and the same thing for all subdirectories inside #{@path}" if File.directory?(@path) + permissions + else + "#{@permission_type} permissions for that path" + end + end + + def parent_folder + File.dirname(@path) + end + def message "There was an error while trying to #{action} `#{@path}`. " \ - "It is likely that you need to grant #{@permission_type} permissions " \ - "for that path." + "It is likely that you need to grant #{permission_type}." end status_code(23) diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 2bbe53aa25..e3253a942f 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -28,7 +28,8 @@ module Bundler " is a chance you are experiencing a man-in-the-middle attack, but" \ " most likely your system doesn't have the CA certificates needed" \ " for verification. For information about OpenSSL certificates, see" \ - " http://bit.ly/ruby-ssl. To connect without using SSL, edit your Gemfile" \ + " https://railsapps.github.io/openssl-certificate-verify-failed.html." \ + " To connect without using SSL, edit your Gemfile" \ " sources and change 'https' to 'http'." end end diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index db43e0f654..cc615db60c 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -63,34 +63,6 @@ module Bundler def request_issue_report_for(e) Bundler.ui.error <<-EOS.gsub(/^ {8}/, ""), nil, nil --- ERROR REPORT TEMPLATE ------------------------------------------------------- - # Error Report - - ## Questions - - Please fill out answers to these questions, it'll help us figure out - why things are going wrong. - - - **What did you do?** - - I ran the command `#{$PROGRAM_NAME} #{ARGV.join(" ")}` - - - **What did you expect to happen?** - - I expected Bundler to... - - - **What happened instead?** - - Instead, what happened was... - - - **Have you tried any solutions posted on similar issues in our issue tracker, stack overflow, or google?** - - I tried... - - - **Have you read our issues document, https://github.com/rubygems/rubygems/blob/master/bundler/doc/contributing/ISSUES.md?** - - ... - - ## Backtrace ``` #{e.class}: #{e.message} @@ -109,8 +81,7 @@ module Bundler First, try this link to see if there are any existing issue reports for this error: #{issues_url(e)} - If there aren't any reports for this error yet, please copy and paste the report template above into a new issue. Don't forget to anonymize any private data! The new issue form is located at: - https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md + If there aren't any reports for this error yet, please fill in the new issue form located at #{new_issue_url}, and copy and paste the report template above in there. EOS end @@ -121,6 +92,10 @@ module Bundler "https://github.com/rubygems/rubygems/search?q=" \ "#{CGI.escape(message)}&type=Issues" end + + def new_issue_url + "https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md" + end end def self.with_friendly_errors diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index 8e1dd7c4f2..60b9e57887 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -98,10 +98,7 @@ module Bundler built_gem_path ||= build_gem cmd = [*gem_command, "install", built_gem_path.to_s] cmd << "--local" if local - _, status = sh_with_status(cmd) - unless status.success? - raise "Couldn't install gem, run `gem install #{built_gem_path}' for more detailed output" - end + sh(cmd) Bundler.ui.confirm "#{name} (#{version}) installed." end @@ -110,7 +107,7 @@ module Bundler SharedHelpers.filesystem_access(File.join(base, "checksums")) {|p| FileUtils.mkdir_p(p) } file_name = "#{File.basename(built_gem_path)}.sha512" require "digest/sha2" - checksum = Digest::SHA512.new.hexdigest(built_gem_path.to_s) + checksum = ::Digest::SHA512.new.hexdigest(built_gem_path.to_s) target = File.join(base, "checksums", file_name) File.write(target, checksum) Bundler.ui.confirm "#{name} #{version} checksum written to checksums/#{file_name}." @@ -132,8 +129,8 @@ module Bundler def git_push(remote = nil) remote ||= default_remote - perform_git_push "#{remote} refs/heads/#{current_branch}" - perform_git_push "#{remote} refs/tags/#{version_tag}" + sh(%W[git push #{remote} refs/heads/#{current_branch}]) + sh(%W[git push #{remote} refs/tags/#{version_tag}]) Bundler.ui.confirm "Pushed git commits and release tag." end @@ -161,13 +158,6 @@ module Bundler allowed_push_host || env_rubygems_host || "rubygems.org" end - def perform_git_push(options = "") - cmd = "git push #{options}" - out, status = sh_with_status(cmd.shellsplit) - return if status.success? - raise "Couldn't git push. `#{cmd}' failed with the following output:\n\n#{out}\n" - end - def already_tagged? return false unless sh(%w[git tag]).split(/\n/).include?(version_tag) Bundler.ui.confirm "Tag #{version_tag} has already been created." @@ -218,8 +208,7 @@ module Bundler def sh(cmd, &block) out, status = sh_with_status(cmd, &block) unless status.success? - cmd = cmd.shelljoin if cmd.respond_to?(:shelljoin) - raise(out.empty? ? "Running `#{cmd}` failed. Run this command directly for more detailed output." : out) + raise("Running `#{cmd.shelljoin}` failed with the following output:\n\n#{out}\n") end out end diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index a59ffe2dec..a3efff1e86 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -85,6 +85,10 @@ module Gem dependencies - development_dependencies end + def deleted_gem? + !default_gem? && !File.directory?(full_gem_path) + end + private def dependencies_to_gemfile(dependencies, group = nil) diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index 8890582318..bb9f1cb3f5 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -16,10 +16,12 @@ module Bundler spec.loaded_from = spec_file # Completely remove any previous gem files - FileUtils.rm_rf gem_dir - FileUtils.rm_rf spec.extension_dir + strict_rm_rf gem_dir + strict_rm_rf spec.extension_dir - FileUtils.mkdir_p gem_dir, :mode => 0o755 + SharedHelpers.filesystem_access(gem_dir, :create) do + FileUtils.mkdir_p gem_dir, :mode => 0o755 + end extract_files @@ -31,7 +33,10 @@ module Bundler generate_plugins write_spec - write_cache_file + + SharedHelpers.filesystem_access("#{gem_home}/cache", :write) do + write_cache_file + end say spec.post_install_message unless spec.post_install_message.nil? @@ -87,6 +92,17 @@ module Bundler private + def strict_rm_rf(dir) + # FileUtils.rm_rf should probably rise in case of permission issues like + # `rm -rf` does. However, it fails to delete the folder silently due to + # https://github.com/ruby/fileutils/issues/57. It should probably be fixed + # inside `fileutils` but for now I`m checking whether the folder was + # removed after it completes, and raising otherwise. + FileUtils.rm_rf dir + + raise PermissionError.new(dir, :delete) if File.directory?(dir) + end + def validate_bundler_checksum(checksum) return true if Bundler.settings[:disable_checksum_validation] return true unless checksum diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 98982b0f07..f6d59baf15 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -86,16 +86,12 @@ module Bundler def spec_missing_extensions?(spec, default = true) return spec.missing_extensions? if spec.respond_to?(:missing_extensions?) - return false if spec_default_gem?(spec) + return false if spec.default_gem? return false if spec.extensions.empty? default end - def spec_default_gem?(spec) - spec.respond_to?(:default_gem?) && spec.default_gem? - end - def spec_matches_for_glob(spec, glob) return spec.matches_for_glob(glob) if spec.respond_to?(:matches_for_glob) @@ -504,14 +500,15 @@ module Bundler end def fetch_specs(remote, name) + require "rubygems/remote_fetcher" path = remote.uri.to_s + "#{name}.#{Gem.marshal_version}.gz" fetcher = gem_remote_fetcher fetcher.headers = { "X-Gemfile-Source" => remote.original_uri.to_s } if remote.original_uri string = fetcher.fetch_path(path) Bundler.load_marshal(string) - rescue Gem::RemoteFetcher::FetchError => e + rescue Gem::RemoteFetcher::FetchError # it's okay for prerelease to fail - raise e unless name == "prerelease_specs" + raise unless name == "prerelease_specs" end def fetch_all_remote_specs(remote) @@ -521,12 +518,32 @@ module Bundler specs.concat(pres) end - def download_gem(spec, uri, path) + def download_gem(spec, uri, cache_dir) + require "rubygems/remote_fetcher" uri = Bundler.settings.mirror_for(uri) fetcher = gem_remote_fetcher fetcher.headers = { "X-Gemfile-Source" => spec.remote.original_uri.to_s } if spec.remote.original_uri Bundler::Retry.new("download gem from #{uri}").attempts do - fetcher.download(spec, uri, path) + gem_file_name = spec.file_name + local_gem_path = File.join cache_dir, gem_file_name + return if File.exist? local_gem_path + + begin + remote_gem_path = uri + "gems/#{gem_file_name}" + remote_gem_path = remote_gem_path.to_s if provides?("< 3.2.0.rc.1") + + SharedHelpers.filesystem_access(local_gem_path) do + fetcher.cache_update_path remote_gem_path, local_gem_path + end + rescue Gem::RemoteFetcher::FetchError + raise if spec.original_platform == spec.platform + + original_gem_file_name = "#{spec.original_name}.gem" + raise if gem_file_name == original_gem_file_name + + gem_file_name = original_gem_file_name + retry + end end rescue Gem::RemoteFetcher::FetchError => e raise Bundler::HTTPError, "Could not download gem from #{uri} due to underlying error <#{e.message}>" diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index fbb8833cfb..31e71388a6 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -291,7 +291,7 @@ module Bundler return unless activated_spec = Bundler.rubygems.loaded_specs(spec.name) return if activated_spec.version == spec.version - suggestion = if Bundler.rubygems.spec_default_gem?(activated_spec) + suggestion = if activated_spec.default_gem? "Since #{spec.name} is a default gem, you can either remove your dependency on it" \ " or try updating to a newer version of bundler that supports #{spec.name} as a default gem." else diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 679fb22574..a41a2f23e9 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -307,7 +307,9 @@ module Bundler # If there is no URI scheme, assume it is an ssh/git URI input = uri end - SharedHelpers.digest(:SHA1).hexdigest(input) + # We use SHA1 here for historical reason and to preserve backward compatibility. + # But a transition to a simpler mangling algorithm would be welcome. + Bundler::Digest.sha1(input) end def cached_revision diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 7f8699b91c..11b506b324 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -135,7 +135,7 @@ module Bundler force = opts[:force] ensure_builtin_gems_cached = opts[:ensure_builtin_gems_cached] - if ensure_builtin_gems_cached && builtin_gem?(spec) + if ensure_builtin_gems_cached && spec.default_gem? if !cached_path(spec) cached_built_in_gem(spec) unless spec.remote force = true @@ -174,6 +174,7 @@ module Bundler Bundler.ui.confirm message path = cached_gem(spec) + raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path if requires_sudo? install_path = Bundler.tmp(spec.full_name) bin_path = install_path.join("bin") @@ -233,12 +234,8 @@ module Bundler end def cache(spec, custom_path = nil) - if builtin_gem?(spec) - cached_path = cached_built_in_gem(spec) - else - cached_path = cached_gem(spec) - end - raise GemNotFound, "Missing gem file '#{spec.full_name}.gem'." unless cached_path + cached_path = cached_gem(spec) + raise GemNotFound, "Missing gem file '#{spec.file_name}'." unless cached_path return if File.dirname(cached_path) == Bundler.app_cache.to_s Bundler.ui.info " * #{File.basename(cached_path)}" FileUtils.cp(cached_path, Bundler.app_cache(custom_path)) @@ -352,14 +349,17 @@ module Bundler end def cached_gem(spec) - cached_gem = cached_path(spec) - unless cached_gem - raise Bundler::GemNotFound, "Could not find #{spec.file_name} for installation" + if spec.default_gem? + cached_built_in_gem(spec) + else + cached_path(spec) end - cached_gem end def cached_path(spec) + global_cache_path = download_cache_path(spec) + @caches << global_cache_path if global_cache_path + possibilities = @caches.map {|p| "#{p}/#{spec.file_name}" } possibilities.find {|p| File.exist?(p) } end @@ -459,19 +459,26 @@ module Bundler spec.fetch_platform - download_path = requires_sudo? ? Bundler.tmp(spec.full_name) : rubygems_dir - gem_path = "#{rubygems_dir}/cache/#{spec.full_name}.gem" + cache_path = download_cache_path(spec) || default_cache_path_for(rubygems_dir) + gem_path = "#{cache_path}/#{spec.file_name}" + + if requires_sudo? + download_path = Bundler.tmp(spec.full_name) + download_cache_path = default_cache_path_for(download_path) + else + download_cache_path = cache_path + end - SharedHelpers.filesystem_access("#{download_path}/cache") do |p| + SharedHelpers.filesystem_access(download_cache_path) do |p| FileUtils.mkdir_p(p) end - download_gem(spec, download_path) + download_gem(spec, download_cache_path) if requires_sudo? - SharedHelpers.filesystem_access("#{rubygems_dir}/cache") do |p| + SharedHelpers.filesystem_access(cache_path) do |p| Bundler.mkdir_p(p) end - Bundler.sudo "mv #{download_path}/cache/#{spec.full_name}.gem #{gem_path}" + Bundler.sudo "mv #{download_cache_path}/#{spec.file_name} #{gem_path}" end gem_path @@ -479,16 +486,8 @@ module Bundler Bundler.rm_rf(download_path) if requires_sudo? end - def builtin_gem?(spec) - # Ruby 2.1, where all included gems have this summary - return true if spec.summary =~ /is bundled with Ruby/ - - # Ruby 2.0, where gemspecs are stored in specifications/default/ - spec.loaded_from && spec.loaded_from.include?("specifications/default/") - end - def installed?(spec) - installed_specs[spec].any? + installed_specs[spec].any? && !spec.deleted_gem? end def requires_sudo? @@ -499,6 +498,10 @@ module Bundler Bundler.rubygems.gem_dir end + def default_cache_path_for(dir) + "#{dir}/cache" + end + def cache_path Bundler.app_cache end @@ -511,52 +514,13 @@ module Bundler # @param [Specification] spec # the spec we want to download or retrieve from the cache. # - # @param [String] download_path + # @param [String] download_cache_path # the local directory the .gem will end up in. # - def download_gem(spec, download_path) - local_path = File.join(download_path, "cache/#{spec.full_name}.gem") - - if (cache_path = download_cache_path(spec)) && cache_path.file? - SharedHelpers.filesystem_access(local_path) do - FileUtils.cp(cache_path, local_path) - end - else - uri = spec.remote.uri - Bundler.ui.confirm("Fetching #{version_message(spec)}") - rubygems_local_path = Bundler.rubygems.download_gem(spec, uri, download_path) - - # older rubygems return varying file:// variants depending on version - rubygems_local_path = rubygems_local_path.gsub(/\Afile:/, "") unless Bundler.rubygems.provides?(">= 3.2.0.rc.2") - rubygems_local_path = rubygems_local_path.gsub(%r{\A//}, "") if Bundler.rubygems.provides?("< 3.1.0") - - if rubygems_local_path != local_path - SharedHelpers.filesystem_access(local_path) do - FileUtils.mv(rubygems_local_path, local_path) - end - end - cache_globally(spec, local_path) - end - end - - # Checks if the requested spec exists in the global cache. If it does - # not, we create the relevant global cache subdirectory if it does not - # exist and copy the spec from the local cache to the global cache. - # - # @param [Specification] spec - # the spec we want to copy to the global cache. - # - # @param [String] local_cache_path - # the local directory from which we want to copy the .gem. - # - def cache_globally(spec, local_cache_path) - return unless cache_path = download_cache_path(spec) - return if cache_path.exist? - - SharedHelpers.filesystem_access(cache_path.dirname, &:mkpath) - SharedHelpers.filesystem_access(cache_path) do - FileUtils.cp(local_cache_path, cache_path) - end + def download_gem(spec, download_cache_path) + uri = spec.remote.uri + Bundler.ui.confirm("Fetching #{version_message(spec)}") + Bundler.rubygems.download_gem(spec, uri, download_cache_path) end # Returns the global cache path of the calling Rubygems::Source object. @@ -575,7 +539,7 @@ module Bundler return unless remote = spec.remote return unless cache_slug = remote.cache_slug - Bundler.user_cache.join("gems", cache_slug, spec.file_name) + Bundler.user_cache.join("gems", cache_slug) end def extension_cache_slug(spec) diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb index fbcd26c765..984c1c3dcb 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb @@ -1,14 +1,18 @@ -require_relative 'connection_pool/version' -require_relative 'connection_pool/timed_stack' +require "timeout" +require_relative "connection_pool/version" +class Bundler::ConnectionPool + class Error < ::RuntimeError; end + class PoolShuttingDownError < ::Bundler::ConnectionPool::Error; end + class TimeoutError < ::Timeout::Error; end +end -# Generic connection pool class for e.g. sharing a limited number of network connections -# among many threads. Note: Connections are lazily created. +# Generic connection pool class for sharing a limited number of objects or network connections +# among many threads. Note: pool elements are lazily created. # # Example usage with block (faster): # # @pool = Bundler::ConnectionPool.new { Redis.new } -# # @pool.with do |redis| # redis.lpop('my-list') if redis.llen('my-list') > 0 # end @@ -34,29 +38,23 @@ require_relative 'connection_pool/timed_stack' class Bundler::ConnectionPool DEFAULTS = {size: 5, timeout: 5} - class Error < RuntimeError - end - def self.wrap(options, &block) Wrapper.new(options, &block) end def initialize(options = {}, &block) - raise ArgumentError, 'Connection pool requires a block' unless block + raise ArgumentError, "Connection pool requires a block" unless block options = DEFAULTS.merge(options) - @size = options.fetch(:size) + @size = Integer(options.fetch(:size)) @timeout = options.fetch(:timeout) @available = TimedStack.new(@size, &block) - @key = :"current-#{@available.object_id}" - @key_count = :"current-#{@available.object_id}-count" + @key = :"pool-#{@available.object_id}" + @key_count = :"pool-#{@available.object_id}-count" end -if Thread.respond_to?(:handle_interrupt) - - # MRI def with(options = {}) Thread.handle_interrupt(Exception => :never) do conn = checkout(options) @@ -69,28 +67,15 @@ if Thread.respond_to?(:handle_interrupt) end end end - -else - - # jruby 1.7.x - def with(options = {}) - conn = checkout(options) - begin - yield conn - ensure - checkin - end - end - -end + alias then with def checkout(options = {}) if ::Thread.current[@key] - ::Thread.current[@key_count]+= 1 + ::Thread.current[@key_count] += 1 ::Thread.current[@key] else - ::Thread.current[@key_count]= 1 - ::Thread.current[@key]= @available.pop(options[:timeout] || @timeout) + ::Thread.current[@key_count] = 1 + ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout) end end @@ -98,64 +83,44 @@ end if ::Thread.current[@key] if ::Thread.current[@key_count] == 1 @available.push(::Thread.current[@key]) - ::Thread.current[@key]= nil + ::Thread.current[@key] = nil + ::Thread.current[@key_count] = nil else - ::Thread.current[@key_count]-= 1 + ::Thread.current[@key_count] -= 1 end else - raise Bundler::ConnectionPool::Error, 'no connections are checked out' + raise Bundler::ConnectionPool::Error, "no connections are checked out" end nil end + ## + # Shuts down the Bundler::ConnectionPool by passing each connection to +block+ and + # then removing it from the pool. Attempting to checkout a connection after + # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+. + def shutdown(&block) @available.shutdown(&block) end - # Size of this connection pool - def size - @size + ## + # Reloads the Bundler::ConnectionPool by passing each connection to +block+ and then + # removing it the pool. Subsequent checkouts will create new connections as + # needed. + + def reload(&block) + @available.shutdown(reload: true, &block) end + # Size of this connection pool + attr_reader :size + # Number of pool entries available for checkout at this instant. def available @available.length end - - private - - class Wrapper < ::BasicObject - METHODS = [:with, :pool_shutdown] - - def initialize(options = {}, &block) - @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(options, &block) } - end - - def with(&block) - @pool.with(&block) - end - - def pool_shutdown(&block) - @pool.shutdown(&block) - end - - def pool_size - @pool.size - end - - def pool_available - @pool.available - end - - def respond_to?(id, *args) - METHODS.include?(id) || with { |c| c.respond_to?(id, *args) } - end - - def method_missing(name, *args, &block) - with do |connection| - connection.send(name, *args, &block) - end - end - end end + +require_relative "connection_pool/timed_stack" +require_relative "connection_pool/wrapper" diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/monotonic_time.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/monotonic_time.rb deleted file mode 100644 index 5a9c4a27bb..0000000000 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/monotonic_time.rb +++ /dev/null @@ -1,66 +0,0 @@ -# Global monotonic clock from Concurrent Ruby 1.0. -# Copyright (c) Jerry D'Antonio -- released under the MIT license. -# Slightly modified; used with permission. -# https://github.com/ruby-concurrency/concurrent-ruby - -require 'thread' - -class Bundler::ConnectionPool - - class_definition = Class.new do - - if defined?(Process::CLOCK_MONOTONIC) - - # @!visibility private - def get_time - Process.clock_gettime(Process::CLOCK_MONOTONIC) - end - - elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' - - # @!visibility private - def get_time - java.lang.System.nanoTime() / 1_000_000_000.0 - end - - else - - # @!visibility private - def initialize - @mutex = Mutex.new - @last_time = Time.now.to_f - end - - # @!visibility private - def get_time - @mutex.synchronize do - now = Time.now.to_f - if @last_time < now - @last_time = now - else # clock has moved back in time - @last_time += 0.000_001 - end - end - end - end - end - - ## - # Clock that cannot be set and represents monotonic time since - # some unspecified starting point. - # - # @!visibility private - GLOBAL_MONOTONIC_CLOCK = class_definition.new - private_constant :GLOBAL_MONOTONIC_CLOCK - - class << self - ## - # Returns the current time a tracked by the application monotonic clock. - # - # @return [Float] The current monotonic time when `since` not given else - # the elapsed monotonic time between `since` and the current time - def monotonic_time - GLOBAL_MONOTONIC_CLOCK.get_time - end - end -end diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb index f3fe1e04ad..a7b1cf06a8 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb @@ -1,13 +1,3 @@ -require 'thread' -require 'timeout' -require_relative 'monotonic_time' - -## -# Raised when you attempt to retrieve a connection from a pool that has been -# shut down. - -class Bundler::ConnectionPool::PoolShuttingDownError < RuntimeError; end - ## # The TimedStack manages a pool of homogeneous connections (or any resource # you wish to manage). Connections are created lazily up to a given maximum @@ -25,7 +15,7 @@ class Bundler::ConnectionPool::PoolShuttingDownError < RuntimeError; end # # conn = ts.pop # ts.pop timeout: 5 -# #=> raises Timeout::Error after 5 seconds +# #=> raises Bundler::ConnectionPool::TimeoutError after 5 seconds class Bundler::ConnectionPool::TimedStack attr_reader :max @@ -39,8 +29,8 @@ class Bundler::ConnectionPool::TimedStack @created = 0 @que = [] @max = size - @mutex = Mutex.new - @resource = ConditionVariable.new + @mutex = Thread::Mutex.new + @resource = Thread::ConditionVariable.new @shutdown_block = nil end @@ -59,12 +49,12 @@ class Bundler::ConnectionPool::TimedStack @resource.broadcast end end - alias_method :<<, :push + alias << push ## # Retrieves a connection from the stack. If a connection is available it is # immediately returned. If no connection is available within the given - # timeout a Timeout::Error is raised. + # timeout a Bundler::ConnectionPool::TimeoutError is raised. # # +:timeout+ is the only checked entry in +options+ and is preferred over # the +timeout+ argument (which will be removed in a future release). Other @@ -74,7 +64,7 @@ class Bundler::ConnectionPool::TimedStack options, timeout = timeout, 0.5 if Hash === timeout timeout = options.fetch :timeout, timeout - deadline = Bundler::ConnectionPool.monotonic_time + timeout + deadline = current_time + timeout @mutex.synchronize do loop do raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block @@ -83,18 +73,20 @@ class Bundler::ConnectionPool::TimedStack connection = try_create(options) return connection if connection - to_wait = deadline - Bundler::ConnectionPool.monotonic_time - raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0 + to_wait = deadline - current_time + raise Bundler::ConnectionPool::TimeoutError, "Waited #{timeout} sec" if to_wait <= 0 @resource.wait(@mutex, to_wait) end end end ## - # Shuts down the TimedStack which prevents connections from being checked - # out. The +block+ is called once for each connection on the stack. + # Shuts down the TimedStack by passing each connection to +block+ and then + # removing it from the pool. Attempting to checkout a connection after + # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+ unless + # +:reload+ is +true+. - def shutdown(&block) + def shutdown(reload: false, &block) raise ArgumentError, "shutdown must receive a block" unless block_given? @mutex.synchronize do @@ -102,6 +94,7 @@ class Bundler::ConnectionPool::TimedStack @resource.broadcast shutdown_connections + @shutdown_block = nil if reload end end @@ -121,6 +114,10 @@ class Bundler::ConnectionPool::TimedStack private + def current_time + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + ## # This is an extension point for TimedStack and is called with a mutex. # @@ -149,6 +146,7 @@ class Bundler::ConnectionPool::TimedStack conn = fetch_connection(options) @shutdown_block.call(conn) end + @created = 0 end ## diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb index b149c0e242..56ebf69902 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb @@ -1,3 +1,3 @@ class Bundler::ConnectionPool - VERSION = "2.2.2" + VERSION = "2.3.0" end diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb new file mode 100644 index 0000000000..880170c06b --- /dev/null +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb @@ -0,0 +1,57 @@ +class Bundler::ConnectionPool + class Wrapper < ::BasicObject + METHODS = [:with, :pool_shutdown, :wrapped_pool] + + def initialize(options = {}, &block) + @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(options, &block) } + end + + def wrapped_pool + @pool + end + + def with(&block) + @pool.with(&block) + end + + def pool_shutdown(&block) + @pool.shutdown(&block) + end + + def pool_size + @pool.size + end + + def pool_available + @pool.available + end + + def respond_to?(id, *args) + METHODS.include?(id) || with { |c| c.respond_to?(id, *args) } + end + + # rubocop:disable Style/MethodMissingSuper + # rubocop:disable Style/MissingRespondToMissing + if ::RUBY_VERSION >= "3.0.0" + def method_missing(name, *args, **kwargs, &block) + with do |connection| + connection.send(name, *args, **kwargs, &block) + end + end + elsif ::RUBY_VERSION >= "2.7.0" + ruby2_keywords def method_missing(name, *args, &block) + with do |connection| + connection.send(name, *args, &block) + end + end + else + def method_missing(name, *args, &block) + with do |connection| + connection.send(name, *args, &block) + end + end + end + # rubocop:enable Style/MethodMissingSuper + # rubocop:enable Style/MissingRespondToMissing + end +end diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index 23a568c8ba..5ee93a2266 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false module Bundler - VERSION = "2.2.29".freeze + VERSION = "2.2.30".freeze def self.bundler_major_version @bundler_major_version ||= VERSION.split(".").first.to_i diff --git a/lib/bundler/worker.rb b/lib/bundler/worker.rb index ffa095b228..5e4ee21c51 100644 --- a/lib/bundler/worker.rb +++ b/lib/bundler/worker.rb @@ -21,8 +21,8 @@ module Bundler # @param func [Proc] job to run in inside the worker pool def initialize(size, name, func) @name = name - @request_queue = Queue.new - @response_queue = Queue.new + @request_queue = Thread::Queue.new + @response_queue = Thread::Queue.new @func = func @size = size @threads = nil diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 3c48f2a8ac..dc6746769a 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -8,7 +8,7 @@ require 'rbconfig' module Gem - VERSION = "3.2.29".freeze + VERSION = "3.2.30".freeze end # Must be first since it unloads the prelude from 1.9.2 @@ -178,7 +178,7 @@ module Gem @configuration = nil @gemdeps = nil @loaded_specs = {} - LOADED_SPECS_MUTEX = Mutex.new + LOADED_SPECS_MUTEX = Thread::Mutex.new @path_to_default_spec_map = {} @platforms = [] @ruby = nil diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index bdfeb0ba6e..867cb07cca 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -43,6 +43,11 @@ class Gem::Commands::CertCommand < Gem::Command options[:key] = open_private_key(key_file) end + add_option('-A', '--key-algorithm ALGORITHM', + 'Select which key algorithm to use for --build') do |algorithm, options| + options[:key_algorithm] = algorithm + end + add_option('-s', '--sign CERT', 'Signs CERT with the key from -K', 'and the certificate from -C') do |cert_file, options| @@ -89,14 +94,14 @@ class Gem::Commands::CertCommand < Gem::Command def open_private_key(key_file) check_openssl passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - key = OpenSSL::PKey::RSA.new File.read(key_file), passphrase + key = OpenSSL::PKey.read File.read(key_file), passphrase raise OptionParser::InvalidArgument, "#{key_file}: private key not found" unless key.private? key rescue Errno::ENOENT raise OptionParser::InvalidArgument, "#{key_file}: does not exist" - rescue OpenSSL::PKey::RSAError - raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA key" + rescue OpenSSL::PKey::PKeyError, ArgumentError + raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key" end def execute @@ -170,7 +175,8 @@ class Gem::Commands::CertCommand < Gem::Command raise Gem::CommandLineError, "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation - key = Gem::Security.create_key + algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM + key = Gem::Security.create_key(algorithm) key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase return key, key_path @@ -255,13 +261,14 @@ For further reading on signing gems see `ri Gem::Security`. key_file = File.join Gem.default_key_path key = File.read key_file passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - options[:key] = OpenSSL::PKey::RSA.new key, passphrase + options[:key] = OpenSSL::PKey.read key, passphrase + rescue Errno::ENOENT alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist" terminate_interaction 1 - rescue OpenSSL::PKey::RSAError + rescue OpenSSL::PKey::PKeyError alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem is not valid" diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 7ef0da4f36..92c9b93fed 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -618,7 +618,7 @@ abort "#{deprecation_message}" end def regenerate_plugins - require "rubygems/commands/pristine_command" + require_relative "pristine_command" say "Regenerating plugins" args = %w[--all --only-plugins --silent] diff --git a/lib/rubygems/core_ext/tcpsocket_init.rb b/lib/rubygems/core_ext/tcpsocket_init.rb index 3d9740c579..2a79b63bd6 100644 --- a/lib/rubygems/core_ext/tcpsocket_init.rb +++ b/lib/rubygems/core_ext/tcpsocket_init.rb @@ -11,10 +11,10 @@ module CoreExtensions IPV4_DELAY_SECONDS = 0.1 def initialize(host, serv, *rest) - mutex = Mutex.new + mutex = Thread::Mutex.new addrs = [] threads = [] - cond_var = ConditionVariable.new + cond_var = Thread::ConditionVariable.new Addrinfo.foreach(host, serv, nil, :STREAM) do |addr| Thread.report_on_exception = false if defined? Thread.report_on_exception = () diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 26ebfaa846..cf5ed7b502 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -68,7 +68,7 @@ class Gem::Installer @path_warning = false - @install_lock = Mutex.new + @install_lock = Thread::Mutex.new class << self ## diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index c4b29e4176..94705914af 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -71,6 +71,13 @@ class Gem::Package end end + class SymlinkError < Error + def initialize(name, destination, destination_dir) + super "installing symlink '%s' pointing to parent path %s of %s is not allowed" % + [name, destination, destination_dir] + end + end + class NonSeekableIO < Error; end class TooLongFileName < Error; end @@ -400,13 +407,21 @@ EOM # extracted. def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: - directories = [] if dir_mode + directories = [] open_tar_gz io do |tar| tar.each do |entry| next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH destination = install_location entry.full_name, destination_dir + if entry.symlink? + link_target = entry.header.linkname + real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination)) + + raise Gem::Package::SymlinkError.new(entry.full_name, real_destination, destination_dir) unless + normalize_path(real_destination).start_with? normalize_path(destination_dir + '/') + end + FileUtils.rm_rf destination mkdir_options = {} @@ -417,9 +432,11 @@ EOM else File.dirname destination end - directories << mkdir if directories - mkdir_p_safe mkdir, mkdir_options, destination_dir, entry.full_name + unless directories.include?(mkdir) + FileUtils.mkdir_p mkdir, **mkdir_options + directories << mkdir + end File.open destination, 'wb' do |out| out.write entry.read @@ -432,8 +449,7 @@ EOM end end - if directories - directories.uniq! + if dir_mode File.chmod(dir_mode, *directories) end end @@ -466,21 +482,11 @@ EOM raise Gem::Package::PathError.new(filename, destination_dir) if filename.start_with? '/' - destination_dir = File.expand_path(File.realpath(destination_dir)) - destination = File.expand_path(File.join(destination_dir, filename)) + destination_dir = File.realpath(destination_dir) + destination = File.expand_path(filename, destination_dir) raise Gem::Package::PathError.new(destination, destination_dir) unless - destination.start_with? destination_dir + '/' - - begin - real_destination = File.expand_path(File.realpath(destination)) - rescue - # it's fine if the destination doesn't exist, because rm -rf'ing it can't cause any damage - nil - else - raise Gem::Package::PathError.new(real_destination, destination_dir) unless - real_destination.start_with? destination_dir + '/' - end + normalize_path(destination).start_with? normalize_path(destination_dir + '/') destination.tap(&Gem::UNTAINT) destination @@ -494,22 +500,6 @@ EOM end end - def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name) - destination_dir = File.realpath(File.expand_path(destination_dir)) - parts = mkdir.split(File::SEPARATOR) - parts.reduce do |path, basename| - path = File.realpath(path) unless path == "" - path = File.expand_path(path + File::SEPARATOR + basename) - lstat = File.lstat path rescue nil - if !lstat || !lstat.directory? - unless normalize_path(path).start_with? normalize_path(destination_dir) and (FileUtils.mkdir path, **mkdir_options rescue false) - raise Gem::Package::PathError.new(file_name, destination_dir) - end - end - path - end - end - ## # Loads a Gem::Specification from the TarEntry +entry+ diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb index ea0f260ab4..0acd5bf9c8 100644 --- a/lib/rubygems/query_utils.rb +++ b/lib/rubygems/query_utils.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'rubygems/local_remote_options' -require 'rubygems/spec_fetcher' -require 'rubygems/version_option' -require 'rubygems/text' +require_relative 'local_remote_options' +require_relative 'spec_fetcher' +require_relative 'version_option' +require_relative 'text' module Gem::QueryUtils diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 22ad89b970..de6f88f39a 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -72,7 +72,7 @@ class Gem::RemoteFetcher # fetching the gem. def initialize(proxy=nil, dns=nil, headers={}) - require 'rubygems/core_ext/tcpsocket_init' if Gem.configuration.ipv4_fallback_enabled + require_relative 'core_ext/tcpsocket_init' if Gem.configuration.ipv4_fallback_enabled require 'net/http' require 'stringio' require 'uri' @@ -81,7 +81,7 @@ class Gem::RemoteFetcher @proxy = proxy @pools = {} - @pool_lock = Mutex.new + @pool_lock = Thread::Mutex.new @cert_files = Gem::Request.get_cert_files @headers = headers diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index 72e25e20ab..d6100c914b 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -96,8 +96,10 @@ class Gem::Request return unless cert case error_number when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then + require 'time' "Certificate #{cert.subject} expired at #{cert.not_after.iso8601}" when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID then + require 'time' "Certificate #{cert.subject} not valid until #{cert.not_before.iso8601}" when OpenSSL::X509::V_ERR_CERT_REJECTED then "Certificate #{cert.subject} is rejected" diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb index 7f3988952c..a4c2929b38 100644 --- a/lib/rubygems/request/connection_pools.rb +++ b/lib/rubygems/request/connection_pools.rb @@ -11,7 +11,7 @@ class Gem::Request::ConnectionPools # :nodoc: @proxy_uri = proxy_uri @cert_files = cert_files @pools = {} - @pool_mutex = Mutex.new + @pool_mutex = Thread::Mutex.new end def pool_for(uri) diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb index 9985bbafa6..f028516db8 100644 --- a/lib/rubygems/request/http_pool.rb +++ b/lib/rubygems/request/http_pool.rb @@ -12,7 +12,7 @@ class Gem::Request::HTTPPool # :nodoc: @http_args = http_args @cert_files = cert_files @proxy_uri = proxy_uri - @queue = SizedQueue.new 1 + @queue = Thread::SizedQueue.new 1 @queue << nil end diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index cff960708c..9286c54221 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -151,7 +151,7 @@ class Gem::RequestSet @prerelease = options[:prerelease] requests = [] - download_queue = Queue.new + download_queue = Thread::Queue.new # Create a thread-safe list of gems to download sorted_requests.each do |req| diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index bba9afc9ff..4d1deee997 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -1,5 +1,4 @@ -require 'digest' -require 'rubygems/openssl' +require_relative 'openssl' ## # S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems @@ -87,7 +86,7 @@ class Gem::S3URISigner "AWS4-HMAC-SHA256", date_time, credential_info, - Digest::SHA256.hexdigest(canonical_request), + OpenSSL::Digest::SHA256.hexdigest(canonical_request), ].join("\n") end @@ -140,8 +139,8 @@ class Gem::S3URISigner def ec2_metadata_credentials_json require 'net/http' - require 'rubygems/request' - require 'rubygems/request/connection_pools' + require_relative 'request' + require_relative 'request/connection_pools' require 'json' iam_info = ec2_metadata_request(EC2_IAM_INFO) diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 28f705549c..8240a1a059 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -152,6 +152,7 @@ require_relative 'openssl' # certificate for EMAIL_ADDR # -C, --certificate CERT Signing certificate for --sign # -K, --private-key KEY Key for --sign or --build +# -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA. # -s, --sign CERT Signs CERT with the key from -K # and the certificate from -C # -d, --days NUMBER_OF_DAYS Days before the certificate expires @@ -317,7 +318,6 @@ require_relative 'openssl' # * Honor extension restrictions # * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 # file, instead of an array embedded in the metadata. -# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1. # # == Original author # @@ -337,17 +337,19 @@ module Gem::Security DIGEST_NAME = 'SHA256' # :nodoc: ## - # Algorithm for creating the key pair used to sign gems + # Length of keys created by RSA and DSA keys - KEY_ALGORITHM = - if defined?(OpenSSL::PKey::RSA) - OpenSSL::PKey::RSA - end + RSA_DSA_KEY_LENGTH = 3072 ## - # Length of keys created by KEY_ALGORITHM + # Default algorithm to use when building a key pair - KEY_LENGTH = 3072 + DEFAULT_KEY_ALGORITHM = 'RSA' + + ## + # Named curve used for Elliptic Curve + + EC_NAME = 'secp384r1' ## # Cipher used to encrypt the key pair used to sign gems. @@ -400,7 +402,7 @@ module Gem::Security serial = 1) cert = OpenSSL::X509::Certificate.new - cert.public_key = key.public_key + cert.public_key = get_public_key(key) cert.version = 2 cert.serial = serial @@ -418,6 +420,24 @@ module Gem::Security cert end + ## + # Gets the right public key from a PKey instance + + def self.get_public_key(key) + return key.public_key unless key.is_a?(OpenSSL::PKey::EC) + + ec_key = OpenSSL::PKey::EC.new(key.group.curve_name) + ec_key.public_key = key.public_key + ec_key + end + + ## + # In Ruby 2.3 EC doesn't implement the private_key? but not the private? method + + if defined?(OpenSSL::PKey::EC) && Gem::Version.new(String.new(RUBY_VERSION)) < Gem::Version.new("2.4.0") + OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?) + end + ## # Creates a self-signed certificate with an issuer and subject from +email+, # a subject alternative name of +email+ and the given +extensions+ for the @@ -459,11 +479,25 @@ module Gem::Security end ## - # Creates a new key pair of the specified +length+ and +algorithm+. The - # default is a 3072 bit RSA key. - - def self.create_key(length = KEY_LENGTH, algorithm = KEY_ALGORITHM) - algorithm.new length + # Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC + # are supported. + + def self.create_key(algorithm) + if defined?(OpenSSL::PKey) + case algorithm.downcase + when 'dsa' + OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH) + when 'rsa' + OpenSSL::PKey::RSA.new(RSA_DSA_KEY_LENGTH) + when 'ec' + domain_key = OpenSSL::PKey::EC.new(EC_NAME) + domain_key.generate_key + domain_key + else + raise Gem::Security::Exception, + "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported." + end + end end ## @@ -492,7 +526,7 @@ module Gem::Security raise Gem::Security::Exception, "incorrect signing key for re-signing " + "#{expired_certificate.subject}" unless - expired_certificate.public_key.to_pem == private_key.public_key.to_pem + expired_certificate.public_key.to_pem == get_public_key(private_key).to_pem unless expired_certificate.subject.to_s == expired_certificate.issuer.to_s diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 9683e55b32..3c3cb647ee 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -115,9 +115,11 @@ class Gem::Security::Policy raise Gem::Security::Exception, 'missing key or signature' end + public_key = Gem::Security.get_public_key(key) + raise Gem::Security::Exception, "certificate #{signer.subject} does not match the signing key" unless - signer.public_key.to_pem == key.public_key.to_pem + signer.public_key.to_pem == public_key.to_pem true end @@ -164,9 +166,9 @@ class Gem::Security::Policy end save_cert = OpenSSL::X509::Certificate.new File.read path - save_dgst = digester.digest save_cert.public_key.to_s + save_dgst = digester.digest save_cert.public_key.to_pem - pkey_str = root.public_key.to_s + pkey_str = root.public_key.to_pem cert_dgst = digester.digest pkey_str raise Gem::Security::Exception, diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index c5c2c4f220..968cf88973 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -83,8 +83,8 @@ class Gem::Security::Signer @digest_name = Gem::Security::DIGEST_NAME @digest_algorithm = Gem::Security.create_digest(@digest_name) - if @key && !@key.is_a?(OpenSSL::PKey::RSA) - @key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase) + if @key && !@key.is_a?(OpenSSL::PKey::PKey) + @key = OpenSSL::PKey.read(File.read(@key), @passphrase) end if @cert_chain @@ -177,8 +177,7 @@ class Gem::Security::Signer disk_cert = File.read(disk_cert_path) rescue nil disk_key_path = File.join(Gem.default_key_path) - disk_key = - OpenSSL::PKey::RSA.new(File.read(disk_key_path), @passphrase) rescue nil + disk_key = OpenSSL::PKey.read(File.read(disk_key_path), @passphrase) rescue nil return unless disk_key diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 816385d139..b5bd6b80e6 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "rubygems/text" +require_relative "text" ## # A Source knows how to list and fetch gems from a RubyGems marshal index. # diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index 9876adc24e..cda5aa8073 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -225,7 +225,7 @@ class Gem::Source::Git < Gem::Source # A hash for the git gem based on the git repository URI. def uri_hash # :nodoc: - require 'digest' # required here to avoid deadlocking in Gem.activate_bin_path (because digest is a gem on 2.5+) + require_relative '../openssl' normalized = if @repository =~ %r{^\w+://(\w+@)?} @@ -235,6 +235,6 @@ class Gem::Source::Git < Gem::Source @repository end - Digest::SHA1.hexdigest normalized + OpenSSL::Digest::SHA1.hexdigest normalized end end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index aaa3b8ce7b..37fa9d0a8e 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -105,7 +105,7 @@ class Gem::Specification < Gem::BasicSpecification # rubocop:disable Style/MutableConstant LOAD_CACHE = {} # :nodoc: # rubocop:enable Style/MutableConstant - LOAD_CACHE_MUTEX = Mutex.new + LOAD_CACHE_MUTEX = Thread::Mutex.new private_constant :LOAD_CACHE if defined? private_constant diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 61d50a9ff8..0ab44fbf6c 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -543,7 +543,7 @@ class Gem::StreamUI # A progress reporter that behaves nicely with threaded downloading. class ThreadedDownloadReporter - MUTEX = Mutex.new + MUTEX = Thread::Mutex.new ## # The current file name being displayed diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index 2a55305172..4363c5adce 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'rubygems/deprecate' +require_relative 'deprecate' ## # This module contains various utility methods as module methods. -- cgit v1.2.3