diff options
author | Jenny Shen <jenny.shen@shopify.com> | 2023-02-15 10:48:35 -0500 |
---|---|---|
committer | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2023-04-12 11:51:03 +0900 |
commit | 332c4b672637c832bfa4ade64994b28de9fa6f64 (patch) | |
tree | 2ab469a568a89aa7b918f9833c125f93c77ace2e | |
parent | ea95ec5443dae90800c0bd33274733323129b5f1 (diff) |
[rubygems/rubygems] Add WebauthnListener response classes
https://github.com/rubygems/rubygems/commit/0e9a26acb1
7 files changed, 254 insertions, 0 deletions
diff --git a/lib/rubygems/webauthn_listener/response.rb b/lib/rubygems/webauthn_listener/response.rb new file mode 100644 index 0000000000..c4ab492f82 --- /dev/null +++ b/lib/rubygems/webauthn_listener/response.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +## +# The WebauthnListener Response class is used by the WebauthnListener to print +# the specified response to the Gem host using the provided socket. It also closes +# the socket after printing the response. +# +# Types of response classes: +# - ResponseOk +# - ResponseNoContent +# - ResponseBadRequest +# - ResponseNotFound +# - ResponseMethodNotAllowed +# +# Example: +# socket = TCPSocket.new(host, port) +# Gem::WebauthnListener::ResponseOk.send(socket, host) +# + +class Gem::WebauthnListener + class Response + attr_reader :host + + def initialize(host) + @host = host + end + + def self.send(socket, host) + socket.print new(host).payload + socket.close + end + + def payload + status_line_and_connection + access_control_headers + content + end + + private + + def status_line_and_connection + <<~RESPONSE + HTTP/1.1 #{status} + Connection: close + RESPONSE + end + + def access_control_headers + return "" unless add_access_control_headers? + <<~RESPONSE + Access-Control-Allow-Origin: #{host} + Access-Control-Allow-Methods: POST + Access-Control-Allow-Headers: Content-Type, Authorization, x-csrf-token + RESPONSE + end + + def content + return "" unless body + <<~RESPONSE + Content-Type: text/plain + Content-Length: #{body.bytesize} + + #{body} + RESPONSE + end + + def status + raise NotImplementedError + end + + def add_access_control_headers? + false + end + + def body; end + end +end diff --git a/lib/rubygems/webauthn_listener/response/response_bad_request.rb b/lib/rubygems/webauthn_listener/response/response_bad_request.rb new file mode 100644 index 0000000000..031c72e08e --- /dev/null +++ b/lib/rubygems/webauthn_listener/response/response_bad_request.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require_relative "../response" + +class Gem::WebauthnListener::ResponseBadRequest < Gem::WebauthnListener::Response + private + + def status + "400 Bad Request" + end + + def body + "missing code parameter" + end +end diff --git a/lib/rubygems/webauthn_listener/response/response_method_not_allowed.rb b/lib/rubygems/webauthn_listener/response/response_method_not_allowed.rb new file mode 100644 index 0000000000..ae071fc242 --- /dev/null +++ b/lib/rubygems/webauthn_listener/response/response_method_not_allowed.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require_relative "../response" + +class Gem::WebauthnListener::ResponseMethodNotAllowed < Gem::WebauthnListener::Response + private + + def status + "405 Method Not Allowed" + end + + def content + <<~RESPONSE + Allow: GET, OPTIONS + RESPONSE + end +end diff --git a/lib/rubygems/webauthn_listener/response/response_no_content.rb b/lib/rubygems/webauthn_listener/response/response_no_content.rb new file mode 100644 index 0000000000..39aad7fe96 --- /dev/null +++ b/lib/rubygems/webauthn_listener/response/response_no_content.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require_relative "../response" + +class Gem::WebauthnListener::ResponseNoContent < Gem::WebauthnListener::Response + private + + def status + "204 No Content" + end + + def add_access_control_headers? + true + end +end diff --git a/lib/rubygems/webauthn_listener/response/response_not_found.rb b/lib/rubygems/webauthn_listener/response/response_not_found.rb new file mode 100644 index 0000000000..c1207cea36 --- /dev/null +++ b/lib/rubygems/webauthn_listener/response/response_not_found.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +require_relative "../response" + +class Gem::WebauthnListener::ResponseNotFound < Gem::WebauthnListener::Response + private + + def status + "404 Not Found" + end +end diff --git a/lib/rubygems/webauthn_listener/response/response_ok.rb b/lib/rubygems/webauthn_listener/response/response_ok.rb new file mode 100644 index 0000000000..c4e7de3e2c --- /dev/null +++ b/lib/rubygems/webauthn_listener/response/response_ok.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require_relative "../response" + +class Gem::WebauthnListener::ResponseOk < Gem::WebauthnListener::Response + private + + def status + "200 OK" + end + + def add_access_control_headers? + true + end + + def body + "success" + end +end diff --git a/test/rubygems/test_webauthn_listener_response.rb b/test/rubygems/test_webauthn_listener_response.rb new file mode 100644 index 0000000000..5820ae9957 --- /dev/null +++ b/test/rubygems/test_webauthn_listener_response.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/webauthn_listener/response/response_ok" +require "rubygems/webauthn_listener/response/response_no_content" +require "rubygems/webauthn_listener/response/response_bad_request" +require "rubygems/webauthn_listener/response/response_not_found" +require "rubygems/webauthn_listener/response/response_method_not_allowed" + +class WebauthnListenerResponseTest < Gem::TestCase + class MockResponse < Gem::WebauthnListener::Response + def payload + "hello world" + end + end + + def setup + super + @host = "rubygems.example" + end + + def test_ok_response_payload + payload = Gem::WebauthnListener::ResponseOk.new(@host).payload + + expected_payload = <<~RESPONSE + HTTP/1.1 200 OK + Connection: close + Access-Control-Allow-Origin: rubygems.example + Access-Control-Allow-Methods: POST + Access-Control-Allow-Headers: Content-Type, Authorization, x-csrf-token + Content-Type: text/plain + Content-Length: 7 + + success + RESPONSE + + assert_equal expected_payload, payload + end + + def test_no_payload_response_payload + payload = Gem::WebauthnListener::ResponseNoContent.new(@host).payload + + expected_payload = <<~RESPONSE + HTTP/1.1 204 No Content + Connection: close + Access-Control-Allow-Origin: rubygems.example + Access-Control-Allow-Methods: POST + Access-Control-Allow-Headers: Content-Type, Authorization, x-csrf-token + RESPONSE + + assert_equal expected_payload, payload + end + + def test_method_not_allowed_response_payload + payload = Gem::WebauthnListener::ResponseMethodNotAllowed.new(@host).payload + + expected_payload = <<~RESPONSE + HTTP/1.1 405 Method Not Allowed + Connection: close + Allow: GET, OPTIONS + RESPONSE + + assert_equal expected_payload, payload + end + + def test_method_not_found_response_payload + payload = Gem::WebauthnListener::ResponseNotFound.new(@host).payload + + expected_payload = <<~RESPONSE + HTTP/1.1 404 Not Found + Connection: close + RESPONSE + + assert_equal expected_payload, payload + end + + def test_bad_request_response_payload + payload = Gem::WebauthnListener::ResponseBadRequest.new(@host).payload + + expected_payload = <<~RESPONSE + HTTP/1.1 400 Bad Request + Connection: close + Content-Type: text/plain + Content-Length: 22 + + missing code parameter + RESPONSE + + assert_equal expected_payload, payload + end + + def test_send_response + server = TCPServer.new "localhost", 5678 + thread = Thread.new do + receive_socket = server.accept + Thread.current[:payload] = receive_socket.read + receive_socket.close + end + + send_socket = TCPSocket.new "localhost", 5678 + MockResponse.send(send_socket, @host) + + thread.join + assert_equal "hello world", thread[:payload] + assert_predicate send_socket, :closed? + end +end |