From 096f6eec3e6be23991e752a6ce56a2efca7a47c9 Mon Sep 17 00:00:00 2001 From: Jenny Shen Date: Mon, 27 Feb 2023 10:07:12 -0500 Subject: [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 --- lib/rubygems/webauthn_listener/response.rb | 167 ++++++++++++++++++++++------- 1 file changed, 129 insertions(+), 38 deletions(-) (limited to 'lib/rubygems/webauthn_listener/response.rb') 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 -- cgit v1.2.3