em-ws-client 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,238 @@
1
+ # encoding: UTF-8
2
+
3
+ module EventMachine::WebSocketCodec
4
+
5
+ # Internal: A WebSocket frame decoder
6
+ # based on RFC 6455
7
+ class Decoder
8
+
9
+ include Protocol
10
+
11
+ def initialize
12
+ @fragmented = false
13
+ @buffer = ""
14
+ @chunks = nil
15
+ @callbacks = {}
16
+ end
17
+
18
+ def onclose █ @callbacks[:close] = block; end
19
+ def onping █ @callbacks[:ping] = block; end
20
+ def onpong █ @callbacks[:pong] = block; end
21
+ def onframe █ @callbacks[:frame] = block; end
22
+ def onerror █ @callbacks[:error] = block; end
23
+
24
+ # Public: Feed the decoder raw data from the wire
25
+ #
26
+ # data - The raw websocket frame data
27
+ #
28
+ # Examples
29
+ #
30
+ # decoder << raw
31
+ #
32
+ # Returns nothing
33
+ def << data
34
+
35
+ # put the data into the buffer, as
36
+ # we might be replaying
37
+ if data
38
+ @buffer << data
39
+ end
40
+
41
+ # Don't do work if we don't have to
42
+ if @buffer.length < 2
43
+ return
44
+ end
45
+
46
+ # decode the first 2 bytes, with
47
+ # opcode, lengthgth, masking bit, and frag bit
48
+ h1, h2 = @buffer.unpack("CC")
49
+
50
+ # check the fragmentation bit to see
51
+ # if this is a message fragment
52
+ fin = ((h1 & 0x80) == 0x80)
53
+
54
+ # used to keep track of our position in the buffer
55
+ offset = 2
56
+
57
+ # see above for possible opcodes
58
+ opcode = (h1 & 0x0F)
59
+
60
+ # the leading length idicator
61
+ length = (h2 & 0x7F)
62
+
63
+ # masking bit, is the data masked with
64
+ # a specified masking key?
65
+ masked = ((h2 & 0x80) == 0x80)
66
+
67
+ # Find errors and fail fast
68
+ if h1 & 0b01110000 != 0
69
+ return emit :error, 1002, "RSV bits must be 0"
70
+ end
71
+
72
+ if opcode > 7
73
+ if !fin
74
+ return emit :error, 1002, "Control frame cannot be fragmented"
75
+ elsif length > 125
76
+ return emit :error, 1002, "Control frame is too large #{length}"
77
+ elsif opcode > 0xA
78
+ return emit :error, 1002, "Unexpected reserved opcode #{opcode}"
79
+ elsif opcode == CLOSE && length == 1
80
+ return emit :error, 1002, "Close control frame with payload of length 1"
81
+ end
82
+ else
83
+ if opcode != CONTINUATION && opcode != TEXT_FRAME && opcode != BINARY_FRAME
84
+ return emit :error, 1002, "Unexpected reserved opcode #{opcode}"
85
+ end
86
+ end
87
+
88
+ # Get the actual size of the payload
89
+ if length > 125
90
+ if length == 126
91
+ length = @buffer.unpack("@#{offset}n").first
92
+ offset += 2
93
+ else
94
+ length = @buffer.unpack("@#{offset}L!>").first
95
+ offset += 8
96
+ end
97
+ end
98
+
99
+ # unpack the masking key
100
+ if masked
101
+ key = @buffer.unpack("@#{offset}N").first
102
+ offset += 4
103
+ end
104
+
105
+ # replay on next frame
106
+ if @buffer.size < (length + offset)
107
+ return false
108
+ end
109
+
110
+ # Read the important bits
111
+ payload = @buffer.unpack("@#{offset}C#{length}")
112
+
113
+ # Unmask the data if it"s masked
114
+ if masked
115
+ payload.bytesize.times do |i|
116
+ payload[i] = ((payload[i] ^ (key >> ((3 - (i % 4)) * 8))) & 0xFF)
117
+ end
118
+ end
119
+
120
+ payload = payload.pack("C*")
121
+
122
+ case opcode
123
+ when CONTINUATION
124
+
125
+ # We shouldn't get a contination without
126
+ # knowing whether or not it's binary or text
127
+ unless @fragmented
128
+ return emit :error, 1002, "Unexepected continuation"
129
+ end
130
+
131
+ if @fragmented == :text
132
+ @chunks << payload.force_encoding("UTF-8")
133
+ else
134
+ @chunks << payload
135
+ end
136
+
137
+ if fin
138
+ if @fragmented == :text && !valid_utf8?(@chunks)
139
+ return emit :error, 1007, "Invalid UTF"
140
+ end
141
+
142
+ emit :frame, @chunks, @fragmented == :binary
143
+ @chunks = nil
144
+ @fragmented = false
145
+ end
146
+
147
+ when TEXT_FRAME
148
+ # We shouldn't get a text frame when we
149
+ # are expecting a continuation
150
+ if @fragmented
151
+ return emit :error, 1002, "Unexepected frame"
152
+ end
153
+
154
+ # emit or buffer
155
+ if fin
156
+ unless valid_utf8?(payload)
157
+ return emit :error, 1007, "Invalid UTF Hmm"
158
+ end
159
+
160
+ emit :frame, payload, false
161
+ else
162
+ @chunks = payload.force_encoding("UTF-8")
163
+ @fragmented = :text
164
+ end
165
+
166
+ when BINARY_FRAME
167
+ # We shouldn't get a text frame when we
168
+ # are expecting a continuation
169
+ if @fragmented
170
+ return emit :error, 1002, "Unexepected frame"
171
+ end
172
+
173
+ # emit or buffer
174
+ if fin
175
+ emit :frame, payload, true
176
+ else
177
+ @chunks = payload
178
+ @fragmented = :binary
179
+ end
180
+
181
+ when CLOSE
182
+ code, explain = payload.unpack("nA*")
183
+ if explain && !valid_utf8?(explain)
184
+ emit :close, 1007
185
+ else
186
+ emit :close, response_close_code(code)
187
+ end
188
+
189
+ when PING
190
+ emit :ping, payload
191
+
192
+ when PONG
193
+ emit :pong, payload
194
+
195
+ end
196
+
197
+ # Remove data we made use of and call back
198
+ # TODO: remove recursion
199
+ @buffer = @buffer[offset + length..-1] || ""
200
+ if not @buffer.empty?
201
+ self << nil
202
+ end
203
+
204
+ end
205
+
206
+ private
207
+
208
+ # trigger event for listener
209
+ def emit event, *args
210
+ if @callbacks.key?(event)
211
+ @callbacks[event].call(*args)
212
+ end
213
+ end
214
+
215
+ # Determine if the close code we received is valid
216
+ # and close if it's not
217
+ def response_close_code code
218
+ case code
219
+ when 1000,1001,1002,1003,1007,1008,1009,1010,1011
220
+ 1000
221
+ when 3000..3999
222
+ 1000
223
+ when 4000..4999
224
+ 1000
225
+ when nil
226
+ 1000
227
+ else
228
+ 1002
229
+ end
230
+ end
231
+
232
+ def valid_utf8? str
233
+ str.force_encoding("UTF-8").valid_encoding?
234
+ end
235
+
236
+ end
237
+
238
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: UTF-8
2
+
3
+ module EventMachine::WebSocketCodec
4
+
5
+ # Internal: Encodes messages into WebSocket frames
6
+ # based on RFC 6455
7
+ class Encoder
8
+
9
+ include Protocol
10
+
11
+ # Encode a standard payload to a hybi10
12
+ # WebSocket frame
13
+ def encode data, opcode=TEXT_FRAME
14
+ frame = []
15
+ frame << (opcode | 0x80)
16
+
17
+ packr = "CC"
18
+
19
+ if opcode == TEXT_FRAME
20
+ data.force_encoding("UTF-8")
21
+
22
+ if !data.valid_encoding?
23
+ raise "Invalid UTF!"
24
+ end
25
+ end
26
+
27
+ # append frame length and mask bit 0x80
28
+ len = data ? data.bytesize : 0
29
+ if len <= 125
30
+ frame << (len | 0x80)
31
+ elsif len < 65536
32
+ frame << (126 | 0x80)
33
+ frame << len
34
+ packr << "n"
35
+ else
36
+ frame << (127 | 0x80)
37
+ frame << len
38
+ packr << "L!>"
39
+ end
40
+
41
+ # generate a masking key
42
+ key = rand(2 ** 31)
43
+
44
+ # mask each byte with the key
45
+ frame << key
46
+ packr << "N"
47
+
48
+ #puts "op #{opcode} len #{len} bytes #{data}"
49
+ # Apply the masking key to every byte
50
+ len.times do |i|
51
+ frame << ((data.getbyte(i) ^ (key >> ((3 - (i % 4)) * 8))) & 0xFF)
52
+ end
53
+
54
+ frame.pack("#{packr}C*")
55
+ end
56
+
57
+ # create a close payload with code
58
+ def close code, message
59
+ encode [code ? code : 1000, message].pack("nA*"), CLOSE
60
+ end
61
+
62
+ # create a ping payload
63
+ def ping data=nil
64
+ encode data, PING
65
+ end
66
+
67
+ # create a pong payload
68
+ def pong data=nil
69
+ encode data, PONG
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,97 @@
1
+ # encoding: UTF-8
2
+
3
+ module EventMachine::WebSocketCodec
4
+
5
+ # Internal: Responsbile for generating the request and
6
+ # validating the response
7
+ class Handshake
8
+
9
+ class HandshakeError < StandardError; end
10
+
11
+ Status = /^HTTP\/1.1 (\d+)/i.freeze
12
+ Header = /^([^:]+):\s*(.+)$/i.freeze
13
+
14
+ attr_accessor :extra
15
+
16
+ def initialize uri, origin="em-ws-client"
17
+ @uri = uri
18
+ @origin = origin
19
+ @buffer = ""
20
+ @complete = false
21
+ @valid = false
22
+ @extra = ""
23
+ end
24
+
25
+ def request
26
+ headers = ["GET #{path} HTTP/1.1"]
27
+ headers << "Connection: keep-alive, Upgrade"
28
+ headers << "Host: #{host}"
29
+ headers << "Sec-WebSocket-Key: #{request_key}"
30
+ headers << "Sec-WebSocket-Version: 13"
31
+ headers << "Origin: #{@origin}"
32
+ headers << "Upgrade: websocket"
33
+ headers << "User-Agent: em-ws-client"
34
+ headers << "\r\n"
35
+
36
+ headers.join "\r\n"
37
+ end
38
+
39
+ def << data
40
+ @buffer << data
41
+
42
+ if @buffer.index "\r\n\r\n"
43
+
44
+ response, @extra = @buffer.split("\r\n\r\n", 2)
45
+
46
+ lines = response.split "\r\n"
47
+
48
+ if Status =~ lines.shift
49
+ if $1.to_i != 101
50
+ raise HandshakeError.new "Received code #{$1}"
51
+ end
52
+ end
53
+
54
+ table = {}
55
+ lines.each do |line|
56
+ header = /^([^:]+):\s*(.+)$/.match(line)
57
+ table[header[1].downcase.strip] = header[2].strip if header
58
+ end
59
+
60
+ @complete = true
61
+ if table["sec-websocket-accept"] == response_key
62
+ @valid = true
63
+ else
64
+ raise HandshakeError.new "Invalid Sec-Websocket-Accept"
65
+ end
66
+ end
67
+ end
68
+
69
+ def complete?
70
+ @complete
71
+ end
72
+
73
+ def valid?
74
+ @valid
75
+ end
76
+
77
+ def host
78
+ @uri.host + (@uri.port ? ":#{@uri.port}" : "")
79
+ end
80
+
81
+ def path
82
+ (@uri.path.empty? ? "/" : @uri.path) + (@uri.query ? "?#{@uri.query}" : "")
83
+ end
84
+
85
+ # Build a unique request key to match against
86
+ def request_key
87
+ @request_key ||= SecureRandom::base64
88
+ end
89
+
90
+ # Build the response key from the given request key
91
+ # for comparison with the response value.
92
+ def response_key
93
+ Base64.encode64(Digest::SHA1.digest("#{request_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11")).chomp
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,12 @@
1
+ module EventMachine
2
+ module WebSocketCodec
3
+ module Protocol
4
+ CONTINUATION = 0x0
5
+ TEXT_FRAME = 0x1
6
+ BINARY_FRAME = 0x2
7
+ CLOSE = 0x8
8
+ PING = 0x9
9
+ PONG = 0xA
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ require "helper"
2
+
3
+ module EM::WebSocketCodec
4
+ describe Decoder do
5
+
6
+ it "should decode an encoded message" do
7
+ dec = Decoder.new
8
+ enc = Encoder.new
9
+
10
+ str = enc.encode("simple message")
11
+ #dec.<<(str).should == "simple message"
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,96 @@
1
+ require "helper"
2
+
3
+ module EM::WebSocketCodec
4
+ describe Handshake do
5
+
6
+ def craft_response status, message, headers
7
+ response = ["HTTP/1.1 #{status} #{message}"]
8
+ headers.each do |key, val|
9
+ response << "#{key}: #{val}"
10
+ end
11
+ response << "\r\n"
12
+ response.join "\r\n"
13
+ end
14
+
15
+ it "should generate a request a 24 byte key" do
16
+ handshake = Handshake.new URI.parse("ws://test.com/test"), "em-test"
17
+ handshake.request_key.length.should == 24
18
+ end
19
+
20
+ it "should craft an HTTP request" do
21
+ handshake = Handshake.new URI.parse("ws://test.com/test"), "em-test"
22
+ handshake.request.length.should > 0
23
+ end
24
+
25
+ it "should handle an good response" do
26
+ handshake = Handshake.new URI.parse("ws://test.com/test"), "em-test"
27
+
28
+ handshake << craft_response(101, "Switching Protocol", {
29
+ "Upgrade" => "WebSocket",
30
+ "Connection" => "Upgrade",
31
+ "Sec-WebSocket-Accept" => handshake.response_key
32
+ })
33
+
34
+ handshake.complete?.should be_true
35
+ handshake.valid?.should be_true
36
+ handshake.extra.should be_empty
37
+ end
38
+
39
+ it "should handle an bad response" do
40
+ handshake = Handshake.new URI.parse("ws://test.com/test"), "em-test"
41
+
42
+ exception = false
43
+ begin
44
+ handshake << craft_response(200, "OK", {
45
+ "Upgrade" => "WebSocket",
46
+ "Connection" => "Upgrade",
47
+ "Sec-WebSocket-Accept" => handshake.response_key
48
+ })
49
+ rescue Handshake::HandshakeError => err
50
+ exception = true
51
+ end
52
+
53
+ handshake.complete?.should be_false
54
+ exception.should be_true
55
+ end
56
+
57
+ it "should handle an chunked response" do
58
+ handshake = Handshake.new URI.parse("ws://test.com/test"), "em-test"
59
+
60
+ response = craft_response(101, "Switching Protocol", {
61
+ "Upgrade" => "WebSocket",
62
+ "Connection" => "Upgrade",
63
+ "Sec-WebSocket-Accept" => handshake.response_key
64
+ })
65
+
66
+ response[0..-2].each_byte do |byte|
67
+ handshake << byte
68
+ handshake.complete?.should be_false
69
+ end
70
+
71
+ handshake << response[-1..-1]
72
+
73
+ handshake.complete?.should be_true
74
+ handshake.valid?.should be_true
75
+ handshake.extra.should be_empty
76
+ end
77
+
78
+ it "should handle an response with framing after it" do
79
+ handshake = Handshake.new URI.parse("ws://test.com/test"), "em-test"
80
+
81
+ response = craft_response(101, "Switching Protocol", {
82
+ "Upgrade" => "WebSocket",
83
+ "Connection" => "Upgrade",
84
+ "Sec-WebSocket-Accept" => handshake.response_key
85
+ })
86
+
87
+ response << "extradata"
88
+ handshake << response
89
+
90
+ handshake.complete?.should be_true
91
+ handshake.valid?.should be_true
92
+ handshake.extra.should == "extradata"
93
+ end
94
+
95
+ end
96
+ end