summaryrefslogtreecommitdiff
path: root/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb
blob: 17baa64fff3aefa1c4bd1ef1b3aaac13d50b8e06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# 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