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.
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.markdown +86 -20
- data/Rakefile +9 -1
- data/autobahn/fuzzer.rb +94 -0
- data/autobahn/report.html +3615 -0
- data/em-ws-client.gemspec +6 -3
- data/lib/em-ws-client.rb +10 -190
- data/lib/em-ws-client/client.rb +300 -0
- data/lib/em-ws-client/decoder.rb +238 -0
- data/lib/em-ws-client/encoder.rb +74 -0
- data/lib/em-ws-client/handshake.rb +97 -0
- data/lib/em-ws-client/protocol.rb +12 -0
- data/spec/codec_spec.rb +15 -0
- data/spec/handshake_spec.rb +96 -0
- data/spec/helper.rb +3 -0
- metadata +16 -20
- data/example/echo.rb +0 -33
- data/lib/codec/draft10decoder.rb +0 -122
- data/lib/codec/draft10encoder.rb +0 -45
- data/spec/em-ws-client.rb +0 -0
@@ -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
|
data/spec/codec_spec.rb
ADDED
@@ -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
|