summaryrefslogtreecommitdiff
path: root/lib/rubygems/webauthn_listener/response.rb
diff options
context:
space:
mode:
authorJenny Shen <jenny.shen@shopify.com>2023-02-27 10:07:12 -0500
committerHiroshi SHIBATA <hsbt@ruby-lang.org>2023-04-12 11:51:07 +0900
commit096f6eec3e6be23991e752a6ce56a2efca7a47c9 (patch)
tree0dbb43af98a5bff96e6606f0a1fd6fff8529c729 /lib/rubygems/webauthn_listener/response.rb
parentef85b6de42c9d73451eb392178e1faa95b002edd (diff)
[rubygems/rubygems] Refactor Webauthn listener response - Makes the response class a wrapper around Net::HTTPResponse - Builds a Net::HTTPResponse upon initialization - to_s returns a string representation of the response to send - Adds a Socket Responder class to send responses given a socket
https://github.com/rubygems/rubygems/commit/7513c220b6 Co-authored-by: Jacques Chester <jacques.chester@shopify.com>
Diffstat (limited to 'lib/rubygems/webauthn_listener/response.rb')
-rw-r--r--lib/rubygems/webauthn_listener/response.rb167
1 files changed, 129 insertions, 38 deletions
diff --git a/lib/rubygems/webauthn_listener/response.rb b/lib/rubygems/webauthn_listener/response.rb
index 8596e7bd69..baa769c4ae 100644
--- a/lib/rubygems/webauthn_listener/response.rb
+++ b/lib/rubygems/webauthn_listener/response.rb
@@ -1,70 +1,161 @@
# 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.
+# The WebauthnListener Response class is used by the WebauthnListener to create
+# responses to be sent to the Gem host. It creates a Net::HTTPResponse instance
+# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`.
+# Net::HTTPResponse instances cannot be directly sent over a socket.
#
# Types of response classes:
-# - ResponseOk
-# - ResponseNoContent
-# - ResponseBadRequest
-# - ResponseNotFound
-# - ResponseMethodNotAllowed
+# - OkResponse
+# - NoContentResponse
+# - BadRequestResponse
+# - NotFoundResponse
+# - MethodNotAllowedResponse
#
-# Example:
-# socket = TCPSocket.new(host, port)
-# Gem::WebauthnListener::ResponseOk.send(socket, host)
+# Example usage:
+#
+# server = TCPServer.new(0)
+# socket = server.accept
+#
+# response = OkResponse.for("https://rubygems.example")
+# socket.print response.to_s
+# socket.close
#
class Gem::WebauthnListener
class Response
- attr_reader :host
+ attr_reader :http_response
+
+ def self.for(host)
+ new(host)
+ end
def initialize(host)
@host = host
+
+ build_http_response
+ end
+
+ def to_s
+ status_line = "HTTP/#{@http_response.http_version} #{@http_response.code} #{@http_response.message}\r\n"
+ headers = @http_response.to_hash.map {|header, value| "#{header}: #{value.join(", ")}\r\n" }.join + "\r\n"
+ body = @http_response.body ? "#{@http_response.body}\n" : ""
+
+ status_line + headers + body
+ end
+
+ private
+
+ # Must be implemented in subclasses
+ def code
+ raise NotImplementedError
+ end
+
+ def reason_phrase
+ raise NotImplementedError
+ end
+
+ def body; end
+
+ def build_http_response
+ response_class = Net::HTTPResponse::CODE_TO_OBJ[code.to_s]
+ @http_response = response_class.new("1.1", code, reason_phrase)
+ @http_response.instance_variable_set(:@read, true)
+
+ add_connection_header
+ add_access_control_headers
+ add_body
+ end
+
+ def add_connection_header
+ @http_response["connection"] = "close"
+ end
+
+ def add_access_control_headers
+ @http_response["access-control-allow-origin"] = @host
+ @http_response["access-control-allow-methods"] = "POST"
+ @http_response["access-control-allow-headers"] = %w[Content-Type Authorization x-csrf-token]
+ end
+
+ def add_body
+ return unless body
+ @http_response["content-type"] = "text/plain"
+ @http_response["content-length"] = body.bytesize
+ @http_response.instance_variable_set(:@body, body)
end
+ end
+
+ class OkResponse < Response
+ private
- def self.send(socket, host)
- socket.print new(host).payload
- socket.close
+ def code
+ 200
end
- def payload
- status_line_and_connection + access_control_headers + content
+ def reason_phrase
+ "OK"
end
+ def body
+ "success"
+ end
+ end
+
+ class NoContentResponse < Response
private
- def status_line_and_connection
- <<~RESPONSE
- HTTP/1.1 #{status}
- Connection: close
- RESPONSE
+ def code
+ 204
end
- def 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
+ def reason_phrase
+ "No Content"
end
+ end
- def content
- return "" unless body
- <<~RESPONSE
- Content-Type: text/plain
- Content-Length: #{body.bytesize}
+ class BadRequestResponse < Response
+ private
- #{body}
- RESPONSE
+ def code
+ 400
end
- def status
- raise NotImplementedError
+ def reason_phrase
+ "Bad Request"
end
- def body; end
+ def body
+ "missing code parameter"
+ end
+ end
+
+ class NotFoundResponse < Response
+ private
+
+ def code
+ 404
+ end
+
+ def reason_phrase
+ "Not Found"
+ end
+ end
+
+ class MethodNotAllowedResponse < Response
+ private
+
+ def code
+ 405
+ end
+
+ def reason_phrase
+ "Method Not Allowed"
+ end
+
+ def add_access_control_headers
+ super
+ @http_response["allow"] = %w[GET OPTIONS]
+ end
end
end