em-ws-client 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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