summaryrefslogtreecommitdiff
path: root/vendor/bundle/ruby/3.4.0/gems/em-websocket-0.5.3/lib/em-websocket/handshake.rb
blob: 12b79044bd3e74ad1c9068a65bb32adb5b2973f7 (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
require "http/parser"
require "uri"

module EventMachine
  module WebSocket

    # Resposible for creating the server handshake response
    class Handshake
      include EM::Deferrable

      attr_reader :parser, :protocol_version

      # Unfortunately drafts 75 & 76 require knowledge of whether the
      # connection is being terminated as ws/wss in order to generate the
      # correct handshake response
      def initialize(secure)
        @parser = Http::Parser.new
        @secure = secure

        @parser.on_headers_complete = proc { |headers|
          @headers = Hash[headers.map { |k,v| [k.downcase, v] }]
        }
      end

      def receive_data(data)
        @parser << data

        if defined? @headers
          process(@headers, @parser.upgrade_data)
        end
      rescue HTTP::Parser::Error => e
        fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
      end

      # Returns the WebSocket upgrade headers as a hash.
      #
      # Keys are strings, unmodified from the request.
      #
      def headers
        @parser.headers
      end

      # The same as headers, except that the hash keys are downcased
      #
      def headers_downcased
        @headers
      end

      # Returns the request path (excluding any query params)
      #
      def path
        @path
      end

      # Returns the query params as a string foo=bar&baz=...
      def query_string
        @query_string
      end

      def query
        Hash[query_string.split('&').map { |c| c.split('=', 2) }]
      end

      # Returns the WebSocket origin header if provided
      #
      def origin
        @headers["origin"] || @headers["sec-websocket-origin"] || nil
      end

      def secure?
        @secure
      end

      private

      def process(headers, remains)
        unless @parser.http_method == "GET"
          raise HandshakeError, "Must be GET request"
        end

        # Validate request path
        #
        # According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an
        # invalid Request-URI should result in a 400 status code, but
        # HandshakeError's currently result in a WebSocket abort. It's not
        # clear which should take precedence, but an abort will do just fine.
        begin
          uri = URI.parse(@parser.request_url)
          @path = uri.path
          @query_string = uri.query || ""
        rescue URI::InvalidURIError
          raise HandshakeError, "Invalid request URI: #{@parser.request_url}"
        end

        # Validate Upgrade
        unless @parser.upgrade?
          raise HandshakeError, "Not an upgrade request"
        end
        upgrade = @headers['upgrade']
        unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
          raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
        end

        # Determine version heuristically
        version = if @headers['sec-websocket-version']
          # Used from drafts 04 onwards
          @headers['sec-websocket-version'].to_i
        elsif @headers['sec-websocket-draft']
          # Used in drafts 01 - 03
          @headers['sec-websocket-draft'].to_i
        elsif @headers['sec-websocket-key1']
          76
        else
          75
        end

        # Additional handling of bytes after the header if required
        case version
        when 75
          if !remains.empty?
            raise HandshakeError, "Extra bytes after header"
          end
        when 76, 1..3
          if remains.length < 8
            # The whole third-key has not been received yet.
            return nil
          elsif remains.length > 8
            raise HandshakeError, "Extra bytes after third key"
          end
          @headers['third-key'] = remains
        end

        handshake_klass = case version
        when 75
          Handshake75
        when 76, 1..3
          Handshake76
        when 5, 6, 7, 8, 13
          Handshake04
        else
          # According to spec should abort the connection
          raise HandshakeError, "Protocol version #{version} not supported"
        end

        upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)

        handler_klass = Handler.klass_factory(version)

        @protocol_version = version
        succeed(upgrade_response, handler_klass)
      rescue HandshakeError => e
        fail(e)
      end
    end
  end
end