File: client.rb

package info (click to toggle)
ruby-websocket-driver 0.6.3-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 188 kB
  • sloc: ruby: 1,203; java: 44; ansic: 33; makefile: 3
file content (140 lines) | stat: -rw-r--r-- 4,006 bytes parent folder | download | duplicates (2)
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
module WebSocket
  class Driver

    class Client < Hybi
      VALID_SCHEMES = %w[ws wss]

      def self.generate_key
        Base64.strict_encode64(SecureRandom.random_bytes(16))
      end

      attr_reader :status, :headers

      def initialize(socket, options = {})
        super

        @ready_state = -1
        @key         = Client.generate_key
        @accept      = Hybi.generate_accept(@key)
        @http        = HTTP::Response.new

        uri = URI.parse(@socket.url)
        unless VALID_SCHEMES.include?(uri.scheme)
          raise URIError, "#{socket.url} is not a valid WebSocket URL"
        end

        host      = uri.host + (uri.port ? ":#{uri.port}" : '')
        path      = (uri.path == '') ? '/' : uri.path
        @pathname = path + (uri.query ? '?' + uri.query : '')

        @headers['Host']                  = host
        @headers['Upgrade']               = 'websocket'
        @headers['Connection']            = 'Upgrade'
        @headers['Sec-WebSocket-Key']     = @key
        @headers['Sec-WebSocket-Version'] = '13'

        if @protocols.size > 0
          @headers['Sec-WebSocket-Protocol'] = @protocols * ', '
        end

        if uri.user
          auth = Base64.strict_encode64([uri.user, uri.password] * ':')
          @headers['Authorization'] = 'Basic ' + auth
        end
      end

      def version
        'hybi-13'
      end

      def proxy(origin, options = {})
        Proxy.new(self, origin, options)
      end

      def start
        return false unless @ready_state == -1
        @socket.write(Driver.encode(handshake_request, :binary))
        @ready_state = 0
        true
      end

      def parse(chunk)
        return if @ready_state == 3
        return super if @ready_state > 0

        @http.parse(chunk)
        return fail_handshake('Invalid HTTP response') if @http.error?
        return unless @http.complete?

        validate_handshake
        return if @ready_state == 3

        open
        parse(@http.body)
      end

    private 

      def handshake_request
        extensions = @extensions.generate_offer
        @headers['Sec-WebSocket-Extensions'] = extensions if extensions

        start   = "GET #{@pathname} HTTP/1.1"
        headers = [start, @headers.to_s, '']
        headers.join("\r\n")
      end

      def fail_handshake(message)
        message = "Error during WebSocket handshake: #{message}"
        @ready_state = 3
        emit(:error, ProtocolError.new(message))
        emit(:close, CloseEvent.new(ERRORS[:protocol_error], message))
      end

      def validate_handshake
        @status  = @http.code
        @headers = Headers.new(@http.headers)

        unless @http.code == 101
          return fail_handshake("Unexpected response code: #{@http.code}")
        end

        upgrade    = @http['Upgrade'] || ''
        connection = @http['Connection'] || ''
        accept     = @http['Sec-WebSocket-Accept'] || ''
        protocol   = @http['Sec-WebSocket-Protocol'] || ''

        if upgrade == ''
          return fail_handshake("'Upgrade' header is missing")
        elsif upgrade.downcase != 'websocket'
          return fail_handshake("'Upgrade' header value is not 'WebSocket'")
        end

        if connection == ''
          return fail_handshake("'Connection' header is missing")
        elsif connection.downcase != 'upgrade'
          return fail_handshake("'Connection' header value is not 'Upgrade'")
        end

        unless accept == @accept
          return fail_handshake('Sec-WebSocket-Accept mismatch')
        end

        unless protocol == ''
          if @protocols.include?(protocol)
            @protocol = protocol
          else
            return fail_handshake('Sec-WebSocket-Protocol mismatch')
          end
        end

        begin
          @extensions.activate(@headers['Sec-WebSocket-Extensions'])
        rescue ::WebSocket::Extensions::ExtensionError => error
          return fail_handshake(error.message)
        end
      end
    end

  end
end