rubygems-update 3.4.11 → 3.4.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b282bb00b57945e0f0754620e2748128cd209c4babfd48a99b65cd4b3a49453
4
- data.tar.gz: c0eff9cc6ff4ab163302bd9157a18ba0da4ff1bbf3a3c739f5e2cae74b5d82c5
3
+ metadata.gz: 630124afddc18f8f7cb265b5de44eca7014d434e5ea8cd6371aefe67c70ea548
4
+ data.tar.gz: 5d6c95dc48dd7a700c98d000f02fc4fd24a6110ad5b4b584e80eb1d2f6a66d32
5
5
  SHA512:
6
- metadata.gz: a8d6983ca3a5bc09424c7182ad4d6467bd7f3d4c464c2145e2948b5a3a8bdc2d9beebfc5a1dccbc29d214fe957fee57493361609f93828f6c90ffa6a49ff6e5a
7
- data.tar.gz: f625b38ddc0dfa4403f2e643a12b300f94fc373c2ba108f21c5be468229aef1bec8f6e31a49061284cd102d22cd15e53b26a71f9ca8af983496317659a4b696a
6
+ metadata.gz: 7b48042d92e123bdd1923b74d3b3453efed7ff69096995605267f0e17d23110c3c33a7e70e288554f5088ab8ea60ffd2264007acffabc19c56b8f1a55304e31a
7
+ data.tar.gz: d5d31837a2e3e200876195b3eb4ce91000ae809a867017fe8264301d045995321adc5f88a8ce30a2a1271fcb26a28d200b51b92359f841a0ed1ad309d506f18c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 3.4.12 / 2023-04-11
2
+
3
+ ## Enhancements:
4
+
5
+ * [Experimental] Add WebAuthn Support to the CLI. Pull request
6
+ [#6560](https://github.com/rubygems/rubygems/pull/6560) by jenshenny
7
+ * Installs bundler 2.4.12 as a default gem.
8
+
1
9
  # 3.4.11 / 2023-04-10
2
10
 
3
11
  ## Enhancements:
data/Manifest.txt CHANGED
@@ -540,6 +540,8 @@ lib/rubygems/util/list.rb
540
540
  lib/rubygems/validator.rb
541
541
  lib/rubygems/version.rb
542
542
  lib/rubygems/version_option.rb
543
+ lib/rubygems/webauthn_listener.rb
544
+ lib/rubygems/webauthn_listener/response.rb
543
545
  rubygems-update.gemspec
544
546
  setup.rb
545
547
  test/rubygems/alternate_cert.pem
@@ -753,6 +755,8 @@ test/rubygems/test_project_sanity.rb
753
755
  test/rubygems/test_remote_fetch_error.rb
754
756
  test/rubygems/test_require.rb
755
757
  test/rubygems/test_rubygems.rb
758
+ test/rubygems/test_webauthn_listener.rb
759
+ test/rubygems/test_webauthn_listener_response.rb
756
760
  test/rubygems/utilities.rb
757
761
  test/rubygems/wrong_key_cert.pem
758
762
  test/rubygems/wrong_key_cert_32.pem
data/bundler/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 2.4.12 (April 11, 2023)
2
+
3
+ ## Enhancements:
4
+
5
+ - Remove reference to `pry` gem from generated `bin/console` file [#6515](https://github.com/rubygems/rubygems/pull/6515)
6
+
1
7
  # 2.4.11 (April 10, 2023)
2
8
 
3
9
  ## Security:
@@ -4,8 +4,8 @@ module Bundler
4
4
  # Represents metadata from when the Bundler gem was built.
5
5
  module BuildMetadata
6
6
  # begin ivars
7
- @built_at = "2023-04-10".freeze
8
- @git_commit_sha = "be1d1b4623".freeze
7
+ @built_at = "2023-04-11".freeze
8
+ @git_commit_sha = "e2cf278db1".freeze
9
9
  @release = true
10
10
  # end ivars
11
11
 
@@ -7,9 +7,5 @@ require "<%= config[:namespaced_path] %>"
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
9
9
 
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
10
  require "irb"
15
11
  IRB.start(__FILE__)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module Bundler
4
- VERSION = "2.4.11".freeze
4
+ VERSION = "2.4.12".freeze
5
5
 
6
6
  def self.bundler_major_version
7
7
  @bundler_major_version ||= VERSION.split(".").first.to_i
@@ -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
- # ignore
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
@@ -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
- ask_otp
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 ask_otp
254
- say "You have enabled multi-factor authentication. Please enter OTP code."
255
- options[:otp] = ask "Code: "
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)
@@ -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
@@ -8,7 +8,7 @@
8
8
  require "rbconfig"
9
9
 
10
10
  module Gem
11
- VERSION = "3.4.11"
11
+ VERSION = "3.4.12"
12
12
  end
13
13
 
14
14
  # Must be first since it unloads the prelude from 1.9.2
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "rubygems-update"
5
- s.version = "3.4.11"
5
+ s.version = "3.4.12"
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
 
@@ -330,6 +330,8 @@ EOF
330
330
  HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
331
331
  HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
332
332
  ]
333
+ @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
334
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
333
335
 
334
336
  @otp_ui = Gem::MockGemUi.new "111111\n"
335
337
  use_ui @otp_ui do
@@ -345,6 +347,8 @@ EOF
345
347
  def test_otp_verified_failure
346
348
  response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
347
349
  @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
350
+ @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
351
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
348
352
 
349
353
  @otp_ui = Gem::MockGemUi.new "111111\n"
350
354
  use_ui @otp_ui do
@@ -357,6 +361,69 @@ EOF
357
361
  assert_equal "111111", @stub_fetcher.last_request["OTP"]
358
362
  end
359
363
 
364
+ def test_with_webauthn_enabled_success
365
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
366
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
367
+ response_success = "Owner added successfully."
368
+ port = 5678
369
+ server = TCPServer.new(port)
370
+
371
+ @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
372
+ @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
373
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
374
+ HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
375
+ ]
376
+
377
+ TCPServer.stub(:new, server) do
378
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
379
+ use_ui @stub_ui do
380
+ @cmd.add_owners("freewill", ["user-new1@example.com"])
381
+ end
382
+ end
383
+ ensure
384
+ server.close
385
+ end
386
+
387
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
388
+ assert_match "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.", @stub_ui.output
389
+ assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output
390
+ assert_equal "Uvh6T57tkWuUnWYo", @stub_fetcher.last_request["OTP"]
391
+ assert_match response_success, @stub_ui.output
392
+ end
393
+
394
+ def test_with_webauthn_enabled_failure
395
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
396
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
397
+ response_success = "Owner added successfully."
398
+ port = 5678
399
+ server = TCPServer.new(port)
400
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
401
+
402
+ @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
403
+ @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
404
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
405
+ HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
406
+ ]
407
+
408
+ TCPServer.stub(:new, server) do
409
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
410
+ use_ui @stub_ui do
411
+ @cmd.add_owners("freewill", ["user-new1@example.com"])
412
+ end
413
+ end
414
+ ensure
415
+ server.close
416
+ end
417
+
418
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
419
+
420
+ assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
421
+ assert_match "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.", @stub_ui.output
422
+ assert_match "ERROR: Security device verification failed: Something went wrong", @stub_ui.error
423
+ refute_match "You are verified with a security device. You may close the browser window.", @stub_ui.output
424
+ refute_match response_success, @stub_ui.output
425
+ end
426
+
360
427
  def test_remove_owners_unathorized_api_key
361
428
  response_forbidden = "The API key doesn't have access"
362
429
  response_success = "Owner removed successfully."
@@ -391,6 +391,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
391
391
  HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
392
392
  HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
393
393
  ]
394
+ @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
395
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
394
396
 
395
397
  @otp_ui = Gem::MockGemUi.new "111111\n"
396
398
  use_ui @otp_ui do
@@ -406,6 +408,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
406
408
  def test_otp_verified_failure
407
409
  response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
408
410
  @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
411
+ @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
412
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
409
413
 
410
414
  @otp_ui = Gem::MockGemUi.new "111111\n"
411
415
  assert_raise Gem::MockGemUi::TermError do
@@ -420,6 +424,71 @@ class TestGemCommandsPushCommand < Gem::TestCase
420
424
  assert_equal "111111", @fetcher.last_request["OTP"]
421
425
  end
422
426
 
427
+ def test_with_webauthn_enabled_success
428
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
429
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
430
+ response_success = "Successfully registered gem: freewill (1.0.0)"
431
+ port = 5678
432
+ server = TCPServer.new(port)
433
+
434
+ @fetcher.data["#{Gem.host}/api/v1/gems"] = [
435
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
436
+ HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
437
+ ]
438
+ @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
439
+
440
+ TCPServer.stub(:new, server) do
441
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
442
+ use_ui @ui do
443
+ @cmd.send_gem(@path)
444
+ end
445
+ end
446
+ ensure
447
+ server.close
448
+ end
449
+
450
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
451
+ assert_match "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.", @ui.output
452
+ assert_match "You are verified with a security device. You may close the browser window.", @ui.output
453
+ assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
454
+ assert_match response_success, @ui.output
455
+ end
456
+
457
+ def test_with_webauthn_enabled_failure
458
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
459
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
460
+ response_success = "Successfully registered gem: freewill (1.0.0)"
461
+ port = 5678
462
+ server = TCPServer.new(port)
463
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
464
+
465
+ @fetcher.data["#{Gem.host}/api/v1/gems"] = [
466
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
467
+ HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
468
+ ]
469
+ @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
470
+
471
+ error = assert_raise Gem::MockGemUi::TermError do
472
+ TCPServer.stub(:new, server) do
473
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
474
+ use_ui @ui do
475
+ @cmd.send_gem(@path)
476
+ end
477
+ end
478
+ ensure
479
+ server.close
480
+ end
481
+ end
482
+ assert_equal 1, error.exit_code
483
+
484
+ assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
485
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
486
+ assert_match "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.", @ui.output
487
+ assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error
488
+ refute_match "You are verified with a security device. You may close the browser window.", @ui.output
489
+ refute_match response_success, @ui.output
490
+ end
491
+
423
492
  def test_sending_gem_unathorized_api_key_with_mfa_enabled
424
493
  response_mfa_enabled = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
425
494
  response_forbidden = "The API key doesn't have access"
@@ -430,6 +499,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
430
499
  HTTPResponseFactory.create(body: response_forbidden, code: 403, msg: "Forbidden"),
431
500
  HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
432
501
  ]
502
+ @fetcher.data["#{@host}/api/v1/webauthn_verification"] =
503
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
433
504
 
434
505
  @fetcher.data["#{@host}/api/v1/api_key"] = HTTPResponseFactory.create(body: "", code: 200, msg: "OK")
435
506
  @cmd.instance_variable_set :@host, @host
@@ -470,6 +541,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
470
541
  @fetcher.data["#{@host}/api/v1/profile/me.yaml"] = [
471
542
  HTTPResponseFactory.create(body: response_profile, code: 200, msg: "OK"),
472
543
  ]
544
+ @fetcher.data["#{@host}/api/v1/webauthn_verification"] =
545
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
473
546
 
474
547
  @cmd.instance_variable_set :@scope, :push_rubygem
475
548
  @cmd.options[:args] = [@path]
@@ -72,6 +72,9 @@ class TestGemCommandsYankCommand < Gem::TestCase
72
72
  HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
73
73
  HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
74
74
  ]
75
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
76
+ @fetcher.data[webauthn_uri] =
77
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
75
78
 
76
79
  @cmd.options[:args] = %w[a]
77
80
  @cmd.options[:added_platform] = true
@@ -93,6 +96,9 @@ class TestGemCommandsYankCommand < Gem::TestCase
93
96
  response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
94
97
  yank_uri = "http://example/api/v1/gems/yank"
95
98
  @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
99
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
100
+ @fetcher.data[webauthn_uri] =
101
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
96
102
 
97
103
  @cmd.options[:args] = %w[a]
98
104
  @cmd.options[:added_platform] = true
@@ -109,6 +115,84 @@ class TestGemCommandsYankCommand < Gem::TestCase
109
115
  assert_equal "111111", @fetcher.last_request["OTP"]
110
116
  end
111
117
 
118
+ def test_with_webauthn_enabled_success
119
+ webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
120
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
121
+ yank_uri = "http://example/api/v1/gems/yank"
122
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
123
+ port = 5678
124
+ server = TCPServer.new(port)
125
+
126
+ @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
127
+ @fetcher.data[yank_uri] = [
128
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
129
+ HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
130
+ ]
131
+
132
+ @cmd.options[:args] = %w[a]
133
+ @cmd.options[:added_platform] = true
134
+ @cmd.options[:version] = req("= 1.0")
135
+
136
+ TCPServer.stub(:new, server) do
137
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
138
+ use_ui @ui do
139
+ @cmd.execute
140
+ end
141
+ end
142
+ ensure
143
+ server.close
144
+ end
145
+
146
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
147
+ assert_match %r{Yanking gem from http://example}, @ui.output
148
+ assert_match "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.", @ui.output
149
+ assert_match "You are verified with a security device. You may close the browser window.", @ui.output
150
+ assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
151
+ assert_match "Successfully yanked", @ui.output
152
+ end
153
+
154
+ def test_with_webauthn_enabled_failure
155
+ webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
156
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
157
+ yank_uri = "http://example/api/v1/gems/yank"
158
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
159
+ port = 5678
160
+ server = TCPServer.new(port)
161
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
162
+
163
+ @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
164
+ @fetcher.data[yank_uri] = [
165
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
166
+ HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
167
+ ]
168
+
169
+ @cmd.options[:args] = %w[a]
170
+ @cmd.options[:added_platform] = true
171
+ @cmd.options[:version] = req("= 1.0")
172
+
173
+ error = assert_raise Gem::MockGemUi::TermError do
174
+ TCPServer.stub(:new, server) do
175
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
176
+ use_ui @ui do
177
+ @cmd.execute
178
+ end
179
+ end
180
+ ensure
181
+ server.close
182
+ end
183
+ end
184
+ assert_equal 1, error.exit_code
185
+
186
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
187
+
188
+ assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
189
+ assert_match %r{Yanking gem from http://example}, @ui.output
190
+ assert_match "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.", @ui.output
191
+ assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error
192
+ refute_match "You are verified with a security device. You may close the browser window.", @ui.output
193
+ refute_match "Successfully yanked", @ui.output
194
+ end
195
+
112
196
  def test_execute_key
113
197
  yank_uri = "http://example/api/v1/gems/yank"
114
198
  @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK")
@@ -230,10 +230,77 @@ class TestGemGemcutterUtilities < Gem::TestCase
230
230
  assert_equal "111111", @fetcher.last_request["OTP"]
231
231
  end
232
232
 
233
- def util_sign_in(response, host = nil, args = [], extra_input = "")
234
- email = "you@example.com"
235
- password = "secret"
236
- profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
233
+ def test_sign_in_with_webauthn_enabled
234
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
235
+ response_fail = "You have enabled multifactor authentication"
236
+ api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
237
+ port = 5678
238
+ server = TCPServer.new(port)
239
+
240
+ TCPServer.stub(:new, server) do
241
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
242
+ util_sign_in(proc do
243
+ @call_count ||= 0
244
+ if (@call_count += 1).odd?
245
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
246
+ else
247
+ HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
248
+ end
249
+ end, nil, [], "", webauthn_verification_url)
250
+ end
251
+ ensure
252
+ server.close
253
+ end
254
+
255
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
256
+ assert_match "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.", @sign_in_ui.output
257
+ assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
258
+ assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
259
+ end
260
+
261
+ def test_sign_in_with_webauthn_enabled_with_error
262
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
263
+ response_fail = "You have enabled multifactor authentication"
264
+ api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
265
+ port = 5678
266
+ server = TCPServer.new(port)
267
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
268
+
269
+ error = assert_raise Gem::MockGemUi::TermError do
270
+ TCPServer.stub(:new, server) do
271
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
272
+ util_sign_in(proc do
273
+ @call_count ||= 0
274
+ if (@call_count += 1).odd?
275
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
276
+ else
277
+ HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
278
+ end
279
+ end, nil, [], "", webauthn_verification_url)
280
+ end
281
+ ensure
282
+ server.close
283
+ end
284
+ end
285
+ assert_equal 1, error.exit_code
286
+
287
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
288
+ assert_match "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.", @sign_in_ui.output
289
+ assert_match "ERROR: Security device verification failed: Something went wrong", @sign_in_ui.error
290
+ refute_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
291
+ refute_match "Signed in with API key:", @sign_in_ui.output
292
+ end
293
+
294
+ def util_sign_in(response, host = nil, args = [], extra_input = "", webauthn_url = nil)
295
+ email = "you@example.com"
296
+ password = "secret"
297
+ profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
298
+ webauthn_response =
299
+ if webauthn_url
300
+ HTTPResponseFactory.create(body: webauthn_url, code: 200, msg: "OK")
301
+ else
302
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
303
+ end
237
304
 
238
305
  if host
239
306
  ENV["RUBYGEMS_HOST"] = host
@@ -244,6 +311,7 @@ class TestGemGemcutterUtilities < Gem::TestCase
244
311
  @fetcher = Gem::FakeFetcher.new
245
312
  @fetcher.data["#{host}/api/v1/api_key"] = response
246
313
  @fetcher.data["#{host}/api/v1/profile/me.yaml"] = profile_response
314
+ @fetcher.data["#{host}/api/v1/webauthn_verification"] = webauthn_response
247
315
  Gem::RemoteFetcher.fetcher = @fetcher
248
316
 
249
317
  @sign_in_ui = Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n" + extra_input)
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require "rubygems/webauthn_listener"
5
+
6
+ class WebauthnListenerTest < Gem::TestCase
7
+ def setup
8
+ super
9
+ @server = TCPServer.new 0
10
+ @port = @server.addr[1].to_s
11
+ end
12
+
13
+ def test_wait_for_otp_code_get_follows_options
14
+ wait_for_otp_code
15
+ assert Gem::MockBrowser.options(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPNoContent
16
+ assert Gem::MockBrowser.get(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPOK
17
+ end
18
+
19
+ def test_wait_for_otp_code_options_request
20
+ wait_for_otp_code
21
+ response = Gem::MockBrowser.options URI("http://localhost:#{@port}?code=xyz")
22
+
23
+ assert response.is_a? Net::HTTPNoContent
24
+ assert_equal Gem.host, response["access-control-allow-origin"]
25
+ assert_equal "POST", response["access-control-allow-methods"]
26
+ assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
27
+ assert_equal "close", response["Connection"]
28
+ end
29
+
30
+ def test_wait_for_otp_code_get_request
31
+ wait_for_otp_code
32
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}?code=xyz")
33
+
34
+ assert response.is_a? Net::HTTPOK
35
+ assert_equal "text/plain", response["Content-Type"]
36
+ assert_equal "7", response["Content-Length"]
37
+ assert_equal Gem.host, response["access-control-allow-origin"]
38
+ assert_equal "POST", response["access-control-allow-methods"]
39
+ assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
40
+ assert_equal "close", response["Connection"]
41
+ assert_equal "success", response.body
42
+
43
+ @thread.join
44
+ assert_equal "xyz", @thread[:otp]
45
+ end
46
+
47
+ def test_wait_for_otp_code_invalid_post_req_method
48
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Invalid HTTP method POST received.")
49
+ response = Gem::MockBrowser.post URI("http://localhost:#{@port}?code=xyz")
50
+
51
+ assert response
52
+ assert response.is_a? Net::HTTPMethodNotAllowed
53
+ assert_equal "GET, OPTIONS", response["allow"]
54
+ assert_equal "close", response["Connection"]
55
+
56
+ @thread.join
57
+ assert_nil @thread[:otp]
58
+ end
59
+
60
+ def test_wait_for_otp_code_incorrect_path
61
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Page at /path not found.")
62
+ response = Gem::MockBrowser.post URI("http://localhost:#{@port}/path?code=xyz")
63
+
64
+ assert response.is_a? Net::HTTPNotFound
65
+ assert_equal "close", response["Connection"]
66
+
67
+ @thread.join
68
+ assert_nil @thread[:otp]
69
+ end
70
+
71
+ def test_wait_for_otp_code_no_params_response
72
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
73
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}")
74
+
75
+ assert response.is_a? Net::HTTPBadRequest
76
+ assert_equal "text/plain", response["Content-Type"]
77
+ assert_equal "22", response["Content-Length"]
78
+ assert_equal "close", response["Connection"]
79
+ assert_equal "missing code parameter", response.body
80
+
81
+ @thread.join
82
+ assert_nil @thread[:otp]
83
+ end
84
+
85
+ def test_wait_for_otp_code_incorrect_params
86
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
87
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}?param=xyz")
88
+
89
+ assert response.is_a? Net::HTTPBadRequest
90
+ assert_equal "text/plain", response["Content-Type"]
91
+ assert_equal "22", response["Content-Length"]
92
+ assert_equal "close", response["Connection"]
93
+ assert_equal "missing code parameter", response.body
94
+
95
+ @thread.join
96
+ assert_nil @thread[:otp]
97
+ end
98
+
99
+ private
100
+
101
+ def wait_for_otp_code
102
+ @thread = Thread.new do
103
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
104
+ end
105
+ @thread.abort_on_exception = true
106
+ @thread.report_on_exception = false
107
+ end
108
+
109
+ def wait_for_otp_code_expect_error_with_message(message)
110
+ @thread = Thread.new do
111
+ error = assert_raise Gem::WebauthnVerificationError do
112
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
113
+ end
114
+
115
+ assert_equal message, error.message
116
+ end
117
+ @thread.abort_on_exception = true
118
+ @thread.report_on_exception = false
119
+ end
120
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require "rubygems/webauthn_listener/response"
5
+
6
+ class WebauthnListenerResponseTest < Gem::TestCase
7
+ def setup
8
+ super
9
+ @host = "rubygems.example"
10
+ end
11
+
12
+ def test_ok_response_to_s
13
+ to_s = Gem::WebauthnListener::OkResponse.new(@host).to_s
14
+
15
+ expected_to_s = <<~RESPONSE
16
+ HTTP/1.1 200 OK\r
17
+ connection: close\r
18
+ access-control-allow-origin: rubygems.example\r
19
+ access-control-allow-methods: POST\r
20
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
21
+ content-type: text/plain\r
22
+ content-length: 7\r
23
+ \r
24
+ success
25
+ RESPONSE
26
+
27
+ assert_equal expected_to_s, to_s
28
+ end
29
+
30
+ def test_no_to_s_response_to_s
31
+ to_s = Gem::WebauthnListener::NoContentResponse.new(@host).to_s
32
+
33
+ expected_to_s = <<~RESPONSE
34
+ HTTP/1.1 204 No Content\r
35
+ connection: close\r
36
+ access-control-allow-origin: rubygems.example\r
37
+ access-control-allow-methods: POST\r
38
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
39
+ \r
40
+ RESPONSE
41
+
42
+ assert_equal expected_to_s, to_s
43
+ end
44
+
45
+ def test_method_not_allowed_response_to_s
46
+ to_s = Gem::WebauthnListener::MethodNotAllowedResponse.new(@host).to_s
47
+
48
+ expected_to_s = <<~RESPONSE
49
+ HTTP/1.1 405 Method Not Allowed\r
50
+ connection: close\r
51
+ access-control-allow-origin: rubygems.example\r
52
+ access-control-allow-methods: POST\r
53
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
54
+ allow: GET, OPTIONS\r
55
+ \r
56
+ RESPONSE
57
+
58
+ assert_equal expected_to_s, to_s
59
+ end
60
+
61
+ def test_method_not_found_response_to_s
62
+ to_s = Gem::WebauthnListener::NotFoundResponse.new(@host).to_s
63
+
64
+ expected_to_s = <<~RESPONSE
65
+ HTTP/1.1 404 Not Found\r
66
+ connection: close\r
67
+ access-control-allow-origin: rubygems.example\r
68
+ access-control-allow-methods: POST\r
69
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
70
+ \r
71
+ RESPONSE
72
+
73
+ assert_equal expected_to_s, to_s
74
+ end
75
+
76
+ def test_bad_request_response_to_s
77
+ to_s = Gem::WebauthnListener::BadRequestResponse.new(@host).to_s
78
+
79
+ expected_to_s = <<~RESPONSE
80
+ HTTP/1.1 400 Bad Request\r
81
+ connection: close\r
82
+ access-control-allow-origin: rubygems.example\r
83
+ access-control-allow-methods: POST\r
84
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
85
+ content-type: text/plain\r
86
+ content-length: 22\r
87
+ \r
88
+ missing code parameter
89
+ RESPONSE
90
+
91
+ assert_equal expected_to_s, to_s
92
+ end
93
+ end
@@ -186,6 +186,41 @@ class Gem::HTTPResponseFactory
186
186
  end
187
187
  end
188
188
 
189
+ ##
190
+ # A Gem::MockBrowser is used in tests to mock a browser in that it can
191
+ # send HTTP requests to the defined URI.
192
+ #
193
+ # Example:
194
+ #
195
+ # # Sends a get request to http://localhost:5678
196
+ # Gem::MockBrowser.get URI("http://localhost:5678")
197
+ #
198
+ # See RubyGems' tests for more examples of MockBrowser.
199
+ #
200
+
201
+ class Gem::MockBrowser
202
+ def self.options(uri)
203
+ options = Net::HTTP::Options.new(uri)
204
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
205
+ http.request(options)
206
+ end
207
+ end
208
+
209
+ def self.get(uri)
210
+ get = Net::HTTP::Get.new(uri)
211
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
212
+ http.request(get)
213
+ end
214
+ end
215
+
216
+ def self.post(uri)
217
+ post = Net::HTTP::Post.new(uri)
218
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
219
+ http.request(post)
220
+ end
221
+ end
222
+ end
223
+
189
224
  # :stopdoc:
190
225
  class Gem::RemoteFetcher
191
226
  def self.fetcher=(fetcher)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubygems-update
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.11
4
+ version: 3.4.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Weirich
@@ -16,7 +16,7 @@ authors:
16
16
  autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
- date: 2023-04-10 00:00:00.000000000 Z
19
+ date: 2023-04-11 00:00:00.000000000 Z
20
20
  dependencies: []
21
21
  description: |-
22
22
  A package (also known as a library) contains a set of functionality
@@ -596,6 +596,8 @@ files:
596
596
  - lib/rubygems/validator.rb
597
597
  - lib/rubygems/version.rb
598
598
  - lib/rubygems/version_option.rb
599
+ - lib/rubygems/webauthn_listener.rb
600
+ - lib/rubygems/webauthn_listener/response.rb
599
601
  - rubygems-update.gemspec
600
602
  - setup.rb
601
603
  - test/rubygems/alternate_cert.pem
@@ -809,6 +811,8 @@ files:
809
811
  - test/rubygems/test_remote_fetch_error.rb
810
812
  - test/rubygems/test_require.rb
811
813
  - test/rubygems/test_rubygems.rb
814
+ - test/rubygems/test_webauthn_listener.rb
815
+ - test/rubygems/test_webauthn_listener_response.rb
812
816
  - test/rubygems/utilities.rb
813
817
  - test/rubygems/wrong_key_cert.pem
814
818
  - test/rubygems/wrong_key_cert_32.pem
@@ -836,7 +840,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
836
840
  - !ruby/object:Gem::Version
837
841
  version: '0'
838
842
  requirements: []
839
- rubygems_version: 3.4.11
843
+ rubygems_version: 3.4.12
840
844
  signing_key:
841
845
  specification_version: 4
842
846
  summary: RubyGems is a package management framework for Ruby.