summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenny Shen <jenny.shen@shopify.com>2023-02-15 10:48:35 -0500
committerHiroshi SHIBATA <hsbt@ruby-lang.org>2023-04-12 11:51:03 +0900
commit332c4b672637c832bfa4ade64994b28de9fa6f64 (patch)
tree2ab469a568a89aa7b918f9833c125f93c77ace2e
parentea95ec5443dae90800c0bd33274733323129b5f1 (diff)
[rubygems/rubygems] Add WebauthnListener response classes
https://github.com/rubygems/rubygems/commit/0e9a26acb1
-rw-r--r--lib/rubygems/webauthn_listener/response.rb75
-rw-r--r--lib/rubygems/webauthn_listener/response/response_bad_request.rb14
-rw-r--r--lib/rubygems/webauthn_listener/response/response_method_not_allowed.rb16
-rw-r--r--lib/rubygems/webauthn_listener/response/response_no_content.rb14
-rw-r--r--lib/rubygems/webauthn_listener/response/response_not_found.rb10
-rw-r--r--lib/rubygems/webauthn_listener/response/response_ok.rb18
-rw-r--r--test/rubygems/test_webauthn_listener_response.rb107
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