lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb



# frozen_string_literal: true

##
# The WebauthnListener Response class is used by the WebauthnListener to create
# responses to be sent to the Gem host. It creates a Gem::Net::HTTPResponse instance
# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`.
# Gem::Net::HTTPResponse instances cannot be directly sent over a socket.
#
# Types of response classes:
#   - OkResponse
#   - NoContentResponse
#   - BadRequestResponse
#   - NotFoundResponse
#   - MethodNotAllowedResponse
#
# Example usage:
#
#   server = TCPServer.new(0)
#   socket = server.accept
#
#   response = OkResponse.for("https://rubygems.example")
#   socket.print response.to_s
#   socket.close
#

module Gem::GemcutterUtilities
  class WebauthnListener
    class Response
      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 = Gem::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; charset=utf-8"
        @http_response["content-length"] = body.bytesize
        @http_response.instance_variable_set(:@body, body)
      end
    end

    class OkResponse < Response
      private

      def code
        200
      end

      def reason_phrase
        "OK"
      end

      def body
        "success"
      end
    end

    class NoContentResponse < Response
      private

      def code
        204
      end

      def reason_phrase
        "No Content"
      end
    end

    class BadRequestResponse < Response
      private

      def code
        400
      end

      def reason_phrase
        "Bad Request"
      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
end