rubygems-update 3.4.10 → 3.4.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/Manifest.txt +5 -0
- data/bundler/CHANGELOG.md +48 -0
- data/bundler/exe/bundle +5 -13
- data/bundler/lib/bundler/build_metadata.rb +2 -2
- data/bundler/lib/bundler/definition.rb +14 -6
- data/bundler/lib/bundler/gem_version_promoter.rb +1 -1
- data/bundler/lib/bundler/installer.rb +1 -1
- data/bundler/lib/bundler/lazy_specification.rb +1 -1
- data/bundler/lib/bundler/lockfile_parser.rb +1 -0
- data/bundler/lib/bundler/man/bundle-cache.1 +2 -2
- data/bundler/lib/bundler/man/bundle-cache.1.ronn +2 -2
- data/bundler/lib/bundler/resolver/base.rb +1 -3
- data/bundler/lib/bundler/ruby_version.rb +1 -1
- data/bundler/lib/bundler/rubygems_ext.rb +5 -3
- data/bundler/lib/bundler/safe_marshal.rb +31 -0
- data/bundler/lib/bundler/settings.rb +3 -2
- data/bundler/lib/bundler/source/rubygems.rb +12 -12
- data/bundler/lib/bundler/spec_set.rb +2 -2
- data/bundler/lib/bundler/templates/newgem/bin/console.tt +0 -4
- data/bundler/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt +5 -0
- data/bundler/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt +1 -1
- data/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt +2 -1
- data/bundler/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb +9 -4
- data/bundler/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb +2 -2
- data/bundler/lib/bundler/vendor/uri/lib/uri/version.rb +1 -1
- data/bundler/lib/bundler/version.rb +1 -1
- data/bundler/lib/bundler.rb +8 -16
- data/lib/rubygems/command_manager.rb +2 -2
- data/lib/rubygems/commands/owner_command.rb +4 -2
- data/lib/rubygems/exceptions.rb +10 -0
- data/lib/rubygems/gemcutter_utilities.rb +48 -6
- data/lib/rubygems/installer.rb +16 -1
- data/lib/rubygems/request_set.rb +2 -2
- data/lib/rubygems/specification.rb +3 -1
- data/lib/rubygems/stub_specification.rb +2 -1
- data/lib/rubygems/webauthn_listener/response.rb +161 -0
- data/lib/rubygems/webauthn_listener.rb +92 -0
- data/lib/rubygems.rb +1 -1
- data/rubygems-update.gemspec +4 -3
- data/test/rubygems/helper.rb +14 -0
- data/test/rubygems/test_bundled_ca.rb +1 -1
- data/test/rubygems/test_config.rb +1 -1
- data/test/rubygems/test_deprecate.rb +1 -1
- data/test/rubygems/test_exit.rb +1 -1
- data/test/rubygems/test_gem.rb +7 -0
- data/test/rubygems/test_gem_commands_owner_command.rb +67 -0
- data/test/rubygems/test_gem_commands_pristine_command.rb +1 -1
- data/test/rubygems/test_gem_commands_push_command.rb +73 -0
- data/test/rubygems/test_gem_commands_setup_command.rb +1 -1
- data/test/rubygems/test_gem_commands_yank_command.rb +84 -0
- data/test/rubygems/test_gem_ext_cargo_builder.rb +1 -0
- data/test/rubygems/test_gem_gem_runner.rb +5 -5
- data/test/rubygems/test_gem_gemcutter_utilities.rb +72 -4
- data/test/rubygems/test_gem_installer.rb +50 -2
- data/test/rubygems/test_gem_uninstaller.rb +4 -4
- data/test/rubygems/test_kernel.rb +1 -1
- data/test/rubygems/test_project_sanity.rb +32 -3
- data/test/rubygems/test_remote_fetch_error.rb +1 -1
- data/test/rubygems/test_webauthn_listener.rb +120 -0
- data/test/rubygems/test_webauthn_listener_response.rb +93 -0
- data/test/rubygems/utilities.rb +43 -3
- metadata +13 -6
data/bundler/lib/bundler.rb
CHANGED
@@ -39,16 +39,6 @@ module Bundler
|
|
39
39
|
environment_preserver.replace_with_backup
|
40
40
|
SUDO_MUTEX = Thread::Mutex.new
|
41
41
|
|
42
|
-
SAFE_MARSHAL_CLASSES = [Symbol, TrueClass, String, Array, Hash, Gem::Version, Gem::Specification].freeze
|
43
|
-
SAFE_MARSHAL_ERROR = "Unexpected class %s present in marshaled data. Only %s are allowed."
|
44
|
-
SAFE_MARSHAL_PROC = proc do |object|
|
45
|
-
object.tap do
|
46
|
-
unless SAFE_MARSHAL_CLASSES.include?(object.class)
|
47
|
-
raise TypeError, format(SAFE_MARSHAL_ERROR, object.class, SAFE_MARSHAL_CLASSES.join(", "))
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
42
|
autoload :Definition, File.expand_path("bundler/definition", __dir__)
|
53
43
|
autoload :Dependency, File.expand_path("bundler/dependency", __dir__)
|
54
44
|
autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__)
|
@@ -86,10 +76,11 @@ module Bundler
|
|
86
76
|
autoload :UI, File.expand_path("bundler/ui", __dir__)
|
87
77
|
autoload :URICredentialsFilter, File.expand_path("bundler/uri_credentials_filter", __dir__)
|
88
78
|
autoload :URINormalizer, File.expand_path("bundler/uri_normalizer", __dir__)
|
79
|
+
autoload :SafeMarshal, File.expand_path("bundler/safe_marshal", __dir__)
|
89
80
|
|
90
81
|
class << self
|
91
82
|
def configure
|
92
|
-
@
|
83
|
+
@configure ||= configure_gem_home_and_path
|
93
84
|
end
|
94
85
|
|
95
86
|
def ui
|
@@ -219,9 +210,10 @@ module Bundler
|
|
219
210
|
end
|
220
211
|
|
221
212
|
def frozen_bundle?
|
222
|
-
frozen = settings[:
|
223
|
-
frozen
|
224
|
-
|
213
|
+
frozen = settings[:frozen]
|
214
|
+
return frozen unless frozen.nil?
|
215
|
+
|
216
|
+
settings[:deployment]
|
225
217
|
end
|
226
218
|
|
227
219
|
def locked_gems
|
@@ -523,7 +515,7 @@ EOF
|
|
523
515
|
end
|
524
516
|
|
525
517
|
def safe_load_marshal(data)
|
526
|
-
load_marshal(data, :marshal_proc =>
|
518
|
+
load_marshal(data, :marshal_proc => SafeMarshal.proc)
|
527
519
|
end
|
528
520
|
|
529
521
|
def load_gemspec(file, validate = false)
|
@@ -581,7 +573,7 @@ EOF
|
|
581
573
|
@bin_path = nil
|
582
574
|
@bundler_major_version = nil
|
583
575
|
@bundle_path = nil
|
584
|
-
@
|
576
|
+
@configure = nil
|
585
577
|
@configured_bundle_path = nil
|
586
578
|
@definition = nil
|
587
579
|
@load = nil
|
@@ -83,7 +83,7 @@ class Gem::CommandManager
|
|
83
83
|
# Return the authoritative instance of the command manager.
|
84
84
|
|
85
85
|
def self.instance
|
86
|
-
@
|
86
|
+
@instance ||= new
|
87
87
|
end
|
88
88
|
|
89
89
|
##
|
@@ -98,7 +98,7 @@ class Gem::CommandManager
|
|
98
98
|
# Reset the authoritative instance of the command manager.
|
99
99
|
|
100
100
|
def self.reset
|
101
|
-
@
|
101
|
+
@instance = nil
|
102
102
|
end
|
103
103
|
|
104
104
|
##
|
@@ -98,8 +98,10 @@ permission to.
|
|
98
98
|
action = method == :delete ? "Removing" : "Adding"
|
99
99
|
|
100
100
|
with_response response, "#{action} #{owner}"
|
101
|
-
rescue
|
102
|
-
|
101
|
+
rescue Gem::WebauthnVerificationError => e
|
102
|
+
raise e
|
103
|
+
rescue StandardError
|
104
|
+
# ignore early exits to allow for completing the iteration of all owners
|
103
105
|
end
|
104
106
|
end
|
105
107
|
end
|
data/lib/rubygems/exceptions.rb
CHANGED
@@ -213,6 +213,16 @@ class Gem::RubyVersionMismatch < Gem::Exception; end
|
|
213
213
|
|
214
214
|
class Gem::VerificationError < Gem::Exception; end
|
215
215
|
|
216
|
+
##
|
217
|
+
# Raised by Gem::WebauthnListener when an error occurs during security
|
218
|
+
# device verification.
|
219
|
+
|
220
|
+
class Gem::WebauthnVerificationError < Gem::Exception
|
221
|
+
def initialize(message)
|
222
|
+
super "Security device verification failed: #{message}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
216
226
|
##
|
217
227
|
# Raised to indicate that a system exit should occur with the specified
|
218
228
|
# exit_code
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require_relative "remote_fetcher"
|
3
3
|
require_relative "text"
|
4
|
+
require_relative "webauthn_listener"
|
4
5
|
|
5
6
|
##
|
6
7
|
# Utility methods for using the RubyGems API.
|
@@ -82,7 +83,7 @@ module Gem::GemcutterUtilities
|
|
82
83
|
#
|
83
84
|
# If +allowed_push_host+ metadata is present, then it will only allow that host.
|
84
85
|
|
85
|
-
def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, &block)
|
86
|
+
def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block)
|
86
87
|
require "net/http"
|
87
88
|
|
88
89
|
self.host = host if host
|
@@ -105,7 +106,7 @@ module Gem::GemcutterUtilities
|
|
105
106
|
response = request_with_otp(method, uri, &block)
|
106
107
|
|
107
108
|
if mfa_unauthorized?(response)
|
108
|
-
|
109
|
+
fetch_otp(credentials)
|
109
110
|
response = request_with_otp(method, uri, &block)
|
110
111
|
end
|
111
112
|
|
@@ -167,11 +168,12 @@ module Gem::GemcutterUtilities
|
|
167
168
|
mfa_params = get_mfa_params(profile)
|
168
169
|
all_params = scope_params.merge(mfa_params)
|
169
170
|
warning = profile["warning"]
|
171
|
+
credentials = { email: email, password: password }
|
170
172
|
|
171
173
|
say "#{warning}\n" if warning
|
172
174
|
|
173
175
|
response = rubygems_api_request(:post, "api/v1/api_key",
|
174
|
-
sign_in_host, scope: scope) do |request|
|
176
|
+
sign_in_host, credentials: credentials, scope: scope) do |request|
|
175
177
|
request.basic_auth email, password
|
176
178
|
request["OTP"] = otp if otp
|
177
179
|
request.body = URI.encode_www_form({ name: key_name }.merge(all_params))
|
@@ -250,9 +252,49 @@ module Gem::GemcutterUtilities
|
|
250
252
|
end
|
251
253
|
end
|
252
254
|
|
253
|
-
def
|
254
|
-
|
255
|
-
|
255
|
+
def fetch_otp(credentials)
|
256
|
+
options[:otp] = if webauthn_url = webauthn_verification_url(credentials)
|
257
|
+
wait_for_otp(webauthn_url)
|
258
|
+
else
|
259
|
+
say "You have enabled multi-factor authentication. Please enter OTP code."
|
260
|
+
ask "Code: "
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def wait_for_otp(webauthn_url)
|
265
|
+
server = TCPServer.new 0
|
266
|
+
port = server.addr[1].to_s
|
267
|
+
|
268
|
+
thread = Thread.new do
|
269
|
+
Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(host, server)
|
270
|
+
rescue Gem::WebauthnVerificationError => e
|
271
|
+
Thread.current[:error] = e
|
272
|
+
end
|
273
|
+
thread.abort_on_exception = true
|
274
|
+
thread.report_on_exception = false
|
275
|
+
|
276
|
+
url_with_port = "#{webauthn_url}?port=#{port}"
|
277
|
+
say "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option."
|
278
|
+
|
279
|
+
thread.join
|
280
|
+
if error = thread[:error]
|
281
|
+
alert_error error.message
|
282
|
+
terminate_interaction(1)
|
283
|
+
end
|
284
|
+
|
285
|
+
say "You are verified with a security device. You may close the browser window."
|
286
|
+
thread[:otp]
|
287
|
+
end
|
288
|
+
|
289
|
+
def webauthn_verification_url(credentials)
|
290
|
+
response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request|
|
291
|
+
if credentials.empty?
|
292
|
+
request.add_field "Authorization", api_key
|
293
|
+
else
|
294
|
+
request.basic_auth credentials[:email], credentials[:password]
|
295
|
+
end
|
296
|
+
end
|
297
|
+
response.is_a?(Net::HTTPSuccess) ? response.body : nil
|
256
298
|
end
|
257
299
|
|
258
300
|
def pretty_host(host)
|
data/lib/rubygems/installer.rb
CHANGED
@@ -342,6 +342,8 @@ class Gem::Installer
|
|
342
342
|
|
343
343
|
Gem::Specification.add_spec(spec)
|
344
344
|
|
345
|
+
load_plugin
|
346
|
+
|
345
347
|
run_post_install_hooks
|
346
348
|
|
347
349
|
spec
|
@@ -388,7 +390,7 @@ class Gem::Installer
|
|
388
390
|
# we'll be installing into.
|
389
391
|
|
390
392
|
def installed_specs
|
391
|
-
@
|
393
|
+
@installed_specs ||= begin
|
392
394
|
specs = []
|
393
395
|
|
394
396
|
Gem::Util.glob_files_in_dir("*.gemspec", File.join(gem_home, "specifications")).each do |path|
|
@@ -1002,4 +1004,17 @@ TEXT
|
|
1002
1004
|
""
|
1003
1005
|
end
|
1004
1006
|
end
|
1007
|
+
|
1008
|
+
def load_plugin
|
1009
|
+
specs = Gem::Specification.find_all_by_name(spec.name)
|
1010
|
+
# If old version already exists, this plugin isn't loaded
|
1011
|
+
# immediately. It's for avoiding a case that multiple versions
|
1012
|
+
# are loaded at the same time.
|
1013
|
+
return unless specs.size == 1
|
1014
|
+
|
1015
|
+
plugin_files = spec.plugins.map do |plugin|
|
1016
|
+
File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}")
|
1017
|
+
end
|
1018
|
+
Gem.load_plugin_files(plugin_files)
|
1019
|
+
end
|
1005
1020
|
end
|
data/lib/rubygems/request_set.rb
CHANGED
@@ -107,7 +107,7 @@ class Gem::RequestSet
|
|
107
107
|
@requests = []
|
108
108
|
@sets = []
|
109
109
|
@soft_missing = false
|
110
|
-
@
|
110
|
+
@sorted_requests = nil
|
111
111
|
@specs = nil
|
112
112
|
@vendor_set = nil
|
113
113
|
@source_set = nil
|
@@ -424,7 +424,7 @@ class Gem::RequestSet
|
|
424
424
|
end
|
425
425
|
|
426
426
|
def sorted_requests
|
427
|
-
@
|
427
|
+
@sorted_requests ||= strongly_connected_components.flatten
|
428
428
|
end
|
429
429
|
|
430
430
|
def specs
|
@@ -2233,7 +2233,7 @@ class Gem::Specification < Gem::BasicSpecification
|
|
2233
2233
|
# The platform this gem runs on. See Gem::Platform for details.
|
2234
2234
|
|
2235
2235
|
def platform
|
2236
|
-
@new_platform ||= Gem::Platform::RUBY
|
2236
|
+
@new_platform ||= Gem::Platform::RUBY # rubocop:disable Naming/MemoizedInstanceVariableName
|
2237
2237
|
end
|
2238
2238
|
|
2239
2239
|
def pretty_print(q) # :nodoc:
|
@@ -2712,6 +2712,8 @@ class Gem::Specification < Gem::BasicSpecification
|
|
2712
2712
|
end
|
2713
2713
|
|
2714
2714
|
@installed_by_version ||= nil
|
2715
|
+
|
2716
|
+
nil
|
2715
2717
|
end
|
2716
2718
|
|
2717
2719
|
def flatten_require_paths # :nodoc:
|
@@ -183,7 +183,7 @@ class Gem::StubSpecification < Gem::BasicSpecification
|
|
183
183
|
##
|
184
184
|
# The full Gem::Specification for this gem, loaded from evalling its gemspec
|
185
185
|
|
186
|
-
def
|
186
|
+
def spec
|
187
187
|
@spec ||= if @data
|
188
188
|
loaded = Gem.loaded_specs[name]
|
189
189
|
loaded if loaded && loaded.version == version
|
@@ -191,6 +191,7 @@ class Gem::StubSpecification < Gem::BasicSpecification
|
|
191
191
|
|
192
192
|
@spec ||= Gem::Specification.load(loaded_from)
|
193
193
|
end
|
194
|
+
alias_method :to_spec, :spec
|
194
195
|
|
195
196
|
##
|
196
197
|
# Is this StubSpecification valid? i.e. have we found a stub line, OR does
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# The WebauthnListener Response class is used by the WebauthnListener to create
|
5
|
+
# responses to be sent to the Gem host. It creates a Net::HTTPResponse instance
|
6
|
+
# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`.
|
7
|
+
# Net::HTTPResponse instances cannot be directly sent over a socket.
|
8
|
+
#
|
9
|
+
# Types of response classes:
|
10
|
+
# - OkResponse
|
11
|
+
# - NoContentResponse
|
12
|
+
# - BadRequestResponse
|
13
|
+
# - NotFoundResponse
|
14
|
+
# - MethodNotAllowedResponse
|
15
|
+
#
|
16
|
+
# Example usage:
|
17
|
+
#
|
18
|
+
# server = TCPServer.new(0)
|
19
|
+
# socket = server.accept
|
20
|
+
#
|
21
|
+
# response = OkResponse.for("https://rubygems.example")
|
22
|
+
# socket.print response.to_s
|
23
|
+
# socket.close
|
24
|
+
#
|
25
|
+
|
26
|
+
class Gem::WebauthnListener
|
27
|
+
class Response
|
28
|
+
attr_reader :http_response
|
29
|
+
|
30
|
+
def self.for(host)
|
31
|
+
new(host)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(host)
|
35
|
+
@host = host
|
36
|
+
|
37
|
+
build_http_response
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
status_line = "HTTP/#{@http_response.http_version} #{@http_response.code} #{@http_response.message}\r\n"
|
42
|
+
headers = @http_response.to_hash.map {|header, value| "#{header}: #{value.join(", ")}\r\n" }.join + "\r\n"
|
43
|
+
body = @http_response.body ? "#{@http_response.body}\n" : ""
|
44
|
+
|
45
|
+
status_line + headers + body
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Must be implemented in subclasses
|
51
|
+
def code
|
52
|
+
raise NotImplementedError
|
53
|
+
end
|
54
|
+
|
55
|
+
def reason_phrase
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
|
59
|
+
def body; end
|
60
|
+
|
61
|
+
def build_http_response
|
62
|
+
response_class = Net::HTTPResponse::CODE_TO_OBJ[code.to_s]
|
63
|
+
@http_response = response_class.new("1.1", code, reason_phrase)
|
64
|
+
@http_response.instance_variable_set(:@read, true)
|
65
|
+
|
66
|
+
add_connection_header
|
67
|
+
add_access_control_headers
|
68
|
+
add_body
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_connection_header
|
72
|
+
@http_response["connection"] = "close"
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_access_control_headers
|
76
|
+
@http_response["access-control-allow-origin"] = @host
|
77
|
+
@http_response["access-control-allow-methods"] = "POST"
|
78
|
+
@http_response["access-control-allow-headers"] = %w[Content-Type Authorization x-csrf-token]
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_body
|
82
|
+
return unless body
|
83
|
+
@http_response["content-type"] = "text/plain"
|
84
|
+
@http_response["content-length"] = body.bytesize
|
85
|
+
@http_response.instance_variable_set(:@body, body)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class OkResponse < Response
|
90
|
+
private
|
91
|
+
|
92
|
+
def code
|
93
|
+
200
|
94
|
+
end
|
95
|
+
|
96
|
+
def reason_phrase
|
97
|
+
"OK"
|
98
|
+
end
|
99
|
+
|
100
|
+
def body
|
101
|
+
"success"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class NoContentResponse < Response
|
106
|
+
private
|
107
|
+
|
108
|
+
def code
|
109
|
+
204
|
110
|
+
end
|
111
|
+
|
112
|
+
def reason_phrase
|
113
|
+
"No Content"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class BadRequestResponse < Response
|
118
|
+
private
|
119
|
+
|
120
|
+
def code
|
121
|
+
400
|
122
|
+
end
|
123
|
+
|
124
|
+
def reason_phrase
|
125
|
+
"Bad Request"
|
126
|
+
end
|
127
|
+
|
128
|
+
def body
|
129
|
+
"missing code parameter"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class NotFoundResponse < Response
|
134
|
+
private
|
135
|
+
|
136
|
+
def code
|
137
|
+
404
|
138
|
+
end
|
139
|
+
|
140
|
+
def reason_phrase
|
141
|
+
"Not Found"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class MethodNotAllowedResponse < Response
|
146
|
+
private
|
147
|
+
|
148
|
+
def code
|
149
|
+
405
|
150
|
+
end
|
151
|
+
|
152
|
+
def reason_phrase
|
153
|
+
"Method Not Allowed"
|
154
|
+
end
|
155
|
+
|
156
|
+
def add_access_control_headers
|
157
|
+
super
|
158
|
+
@http_response["allow"] = %w[GET OPTIONS]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "webauthn_listener/response"
|
4
|
+
|
5
|
+
##
|
6
|
+
# The WebauthnListener class retrieves an OTP after a user successfully WebAuthns with the Gem host.
|
7
|
+
# An instance opens a socket using the TCPServer instance given and listens for a request from the Gem host.
|
8
|
+
# The request should be a GET request to the root path and contains the OTP code in the form
|
9
|
+
# of a query parameter `code`. The listener will return the code which will be used as the OTP for
|
10
|
+
# API requests.
|
11
|
+
#
|
12
|
+
# Types of responses sent by the listener after receiving a request:
|
13
|
+
# - 200 OK: OTP code was successfully retrieved
|
14
|
+
# - 204 No Content: If the request was an OPTIONS request
|
15
|
+
# - 400 Bad Request: If the request did not contain a query parameter `code`
|
16
|
+
# - 404 Not Found: The request was not to the root path
|
17
|
+
# - 405 Method Not Allowed: OTP code was not retrieved because the request was not a GET/OPTIONS request
|
18
|
+
#
|
19
|
+
# Example usage:
|
20
|
+
#
|
21
|
+
# server = TCPServer.new(0)
|
22
|
+
# otp = Gem::WebauthnListener.wait_for_otp_code("https://rubygems.example", server)
|
23
|
+
#
|
24
|
+
|
25
|
+
class Gem::WebauthnListener
|
26
|
+
attr_reader :host
|
27
|
+
|
28
|
+
def initialize(host)
|
29
|
+
@host = host
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.wait_for_otp_code(host, server)
|
33
|
+
new(host).fetch_otp_from_connection(server)
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_otp_from_connection(server)
|
37
|
+
loop do
|
38
|
+
socket = server.accept
|
39
|
+
request_line = socket.gets
|
40
|
+
|
41
|
+
method, req_uri, _protocol = request_line.split(" ")
|
42
|
+
req_uri = URI.parse(req_uri)
|
43
|
+
|
44
|
+
responder = SocketResponder.new(socket)
|
45
|
+
|
46
|
+
unless root_path?(req_uri)
|
47
|
+
responder.send(NotFoundResponse.for(host))
|
48
|
+
raise Gem::WebauthnVerificationError, "Page at #{req_uri.path} not found."
|
49
|
+
end
|
50
|
+
|
51
|
+
case method.upcase
|
52
|
+
when "OPTIONS"
|
53
|
+
responder.send(NoContentResponse.for(host))
|
54
|
+
next # will be GET
|
55
|
+
when "GET"
|
56
|
+
if otp = parse_otp_from_uri(req_uri)
|
57
|
+
responder.send(OkResponse.for(host))
|
58
|
+
return otp
|
59
|
+
end
|
60
|
+
responder.send(BadRequestResponse.for(host))
|
61
|
+
raise Gem::WebauthnVerificationError, "Did not receive OTP from #{host}."
|
62
|
+
else
|
63
|
+
responder.send(MethodNotAllowedResponse.for(host))
|
64
|
+
raise Gem::WebauthnVerificationError, "Invalid HTTP method #{method.upcase} received."
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def root_path?(uri)
|
72
|
+
uri.path == "/"
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_otp_from_uri(uri)
|
76
|
+
require "cgi"
|
77
|
+
|
78
|
+
return if uri.query.nil?
|
79
|
+
CGI.parse(uri.query).dig("code", 0)
|
80
|
+
end
|
81
|
+
|
82
|
+
class SocketResponder
|
83
|
+
def initialize(socket)
|
84
|
+
@socket = socket
|
85
|
+
end
|
86
|
+
|
87
|
+
def send(response)
|
88
|
+
@socket.print response.to_s
|
89
|
+
@socket.close
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/rubygems.rb
CHANGED
data/rubygems-update.gemspec
CHANGED
@@ -2,18 +2,19 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "rubygems-update"
|
5
|
-
s.version = "3.4.
|
5
|
+
s.version = "3.4.14"
|
6
6
|
s.authors = ["Jim Weirich", "Chad Fowler", "Eric Hodel", "Luis Lavena", "Aaron Patterson", "Samuel Giddins", "André Arko", "Evan Phoenix", "Hiroshi SHIBATA"]
|
7
7
|
s.email = ["", "", "drbrain@segment7.net", "luislavena@gmail.com", "aaron@tenderlovemaking.com", "segiddins@segiddins.me", "andre@arko.net", "evan@phx.io", "hsbt@ruby-lang.org"]
|
8
8
|
|
9
|
-
s.summary = "RubyGems is a package management framework for Ruby."
|
9
|
+
s.summary = "RubyGems is a package management framework for Ruby. This gem is downloaded and installed by `gem update --system`, so that the `gem` CLI can update itself."
|
10
10
|
s.description = "A package (also known as a library) contains a set of functionality
|
11
11
|
that can be invoked by a Ruby program, such as reading and parsing an XML file. We call
|
12
12
|
these packages 'gems' and RubyGems is a tool to install, create, manage and load these
|
13
13
|
packages in your Ruby environment. RubyGems is also a client for RubyGems.org, a public
|
14
14
|
repository of Gems that allows you to publish a Gem that can be shared and used by other
|
15
15
|
developers. See our guide on publishing a Gem at guides.rubygems.org"
|
16
|
-
s.homepage = "https://rubygems.org"
|
16
|
+
s.homepage = "https://guides.rubygems.org"
|
17
|
+
s.metadata = { "source_code_uri" => "https://github.com/rubygems/rubygems" }
|
17
18
|
s.licenses = ["Ruby", "MIT"]
|
18
19
|
|
19
20
|
s.files = File.read("Manifest.txt").split
|
data/test/rubygems/helper.rb
CHANGED
@@ -1179,6 +1179,20 @@ Also, a list:
|
|
1179
1179
|
RUBY_PLATFORM.match("mswin")
|
1180
1180
|
end
|
1181
1181
|
|
1182
|
+
##
|
1183
|
+
# Is this test being run on a version of Ruby built with mingw?
|
1184
|
+
|
1185
|
+
def self.mingw_windows?
|
1186
|
+
RUBY_PLATFORM.match("mingw")
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
##
|
1190
|
+
# Is this test being run on a version of Ruby built with mingw?
|
1191
|
+
|
1192
|
+
def mingw_windows?
|
1193
|
+
RUBY_PLATFORM.match("mingw")
|
1194
|
+
end
|
1195
|
+
|
1182
1196
|
##
|
1183
1197
|
# Is this test being run on a ruby/ruby repository?
|
1184
1198
|
#
|
@@ -14,7 +14,7 @@ require "rubygems/request"
|
|
14
14
|
# The tested hosts are explained in detail here: https://github.com/rubygems/rubygems/commit/5e16a5428f973667cabfa07e94ff939e7a83ebd9
|
15
15
|
#
|
16
16
|
|
17
|
-
class
|
17
|
+
class TestGemBundledCA < Gem::TestCase
|
18
18
|
def bundled_certificate_store
|
19
19
|
store = OpenSSL::X509::Store.new
|
20
20
|
|
data/test/rubygems/test_exit.rb
CHANGED
data/test/rubygems/test_gem.rb
CHANGED
@@ -1435,6 +1435,8 @@ class TestGem < Gem::TestCase
|
|
1435
1435
|
def test_load_plugins
|
1436
1436
|
plugin_path = File.join "lib", "rubygems_plugin.rb"
|
1437
1437
|
|
1438
|
+
foo1_plugin_path = nil
|
1439
|
+
foo2_plugin_path = nil
|
1438
1440
|
Dir.chdir @tempdir do
|
1439
1441
|
FileUtils.mkdir_p "lib"
|
1440
1442
|
File.open plugin_path, "w" do |fp|
|
@@ -1444,17 +1446,22 @@ class TestGem < Gem::TestCase
|
|
1444
1446
|
foo1 = util_spec "foo", "1" do |s|
|
1445
1447
|
s.files << plugin_path
|
1446
1448
|
end
|
1449
|
+
foo1_plugin_path = File.join(foo1.gem_dir, plugin_path)
|
1447
1450
|
|
1448
1451
|
install_gem foo1
|
1449
1452
|
|
1450
1453
|
foo2 = util_spec "foo", "2" do |s|
|
1451
1454
|
s.files << plugin_path
|
1452
1455
|
end
|
1456
|
+
foo2_plugin_path = File.join(foo2.gem_dir, plugin_path)
|
1453
1457
|
|
1454
1458
|
install_gem foo2
|
1455
1459
|
end
|
1456
1460
|
|
1457
1461
|
Gem::Specification.reset
|
1462
|
+
PLUGINS_LOADED.clear
|
1463
|
+
$LOADED_FEATURES.delete(foo1_plugin_path)
|
1464
|
+
$LOADED_FEATURES.delete(foo2_plugin_path)
|
1458
1465
|
|
1459
1466
|
gem "foo"
|
1460
1467
|
|