aboutsummaryrefslogtreecommitdiffstats
path: root/lib/rubygems/webauthn_listener/response.rb
blob: baa769c4ae21ca60ef8a1e49ee4d9cb872794a6e (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
# 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 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:
#   - 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
#

class Gem::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 = 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 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