em-websocket 0.4.0 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +35 -1
  3. data/Gemfile +6 -0
  4. data/LICENCE +7 -0
  5. data/README.md +49 -7
  6. data/em-websocket.gemspec +2 -8
  7. data/examples/test.html +3 -1
  8. data/lib/em-websocket/close03.rb +3 -0
  9. data/lib/em-websocket/close05.rb +3 -0
  10. data/lib/em-websocket/close06.rb +3 -0
  11. data/lib/em-websocket/close75.rb +2 -1
  12. data/lib/em-websocket/connection.rb +118 -26
  13. data/lib/em-websocket/framing03.rb +3 -2
  14. data/lib/em-websocket/framing05.rb +3 -2
  15. data/lib/em-websocket/framing07.rb +16 -4
  16. data/lib/em-websocket/framing76.rb +1 -4
  17. data/lib/em-websocket/handler.rb +31 -3
  18. data/lib/em-websocket/handler76.rb +2 -0
  19. data/lib/em-websocket/handshake.rb +23 -4
  20. data/lib/em-websocket/handshake04.rb +10 -6
  21. data/lib/em-websocket/handshake75.rb +11 -1
  22. data/lib/em-websocket/handshake76.rb +4 -0
  23. data/lib/em-websocket/masking04.rb +1 -5
  24. data/lib/em-websocket/message_processor_03.rb +6 -3
  25. data/lib/em-websocket/message_processor_06.rb +30 -9
  26. data/lib/em-websocket/version.rb +1 -1
  27. data/lib/em-websocket/websocket.rb +7 -0
  28. data/spec/helper.rb +67 -52
  29. data/spec/integration/common_spec.rb +49 -32
  30. data/spec/integration/draft03_spec.rb +83 -57
  31. data/spec/integration/draft05_spec.rb +14 -12
  32. data/spec/integration/draft06_spec.rb +67 -7
  33. data/spec/integration/draft13_spec.rb +29 -20
  34. data/spec/integration/draft75_spec.rb +44 -40
  35. data/spec/integration/draft76_spec.rb +58 -46
  36. data/spec/integration/gte_03_examples.rb +42 -0
  37. data/spec/integration/shared_examples.rb +93 -0
  38. data/spec/unit/framing_spec.rb +24 -4
  39. data/spec/unit/handshake_spec.rb +24 -1
  40. data/spec/unit/masking_spec.rb +2 -0
  41. metadata +18 -107
@@ -1,4 +1,5 @@
1
1
  require "http/parser"
2
+ require "uri"
2
3
 
3
4
  module EventMachine
4
5
  module WebSocket
@@ -24,7 +25,7 @@ module EventMachine
24
25
  def receive_data(data)
25
26
  @parser << data
26
27
 
27
- if @headers
28
+ if defined? @headers
28
29
  process(@headers, @parser.upgrade_data)
29
30
  end
30
31
  rescue HTTP::Parser::Error => e
@@ -48,12 +49,12 @@ module EventMachine
48
49
  # Returns the request path (excluding any query params)
49
50
  #
50
51
  def path
51
- @parser.request_path
52
+ @path
52
53
  end
53
54
 
54
55
  # Returns the query params as a string foo=bar&baz=...
55
56
  def query_string
56
- @parser.query_string
57
+ @query_string
57
58
  end
58
59
 
59
60
  def query
@@ -66,6 +67,10 @@ module EventMachine
66
67
  @headers["origin"] || @headers["sec-websocket-origin"] || nil
67
68
  end
68
69
 
70
+ def secure?
71
+ @secure
72
+ end
73
+
69
74
  private
70
75
 
71
76
  def process(headers, remains)
@@ -73,13 +78,27 @@ module EventMachine
73
78
  raise HandshakeError, "Must be GET request"
74
79
  end
75
80
 
81
+ # Validate request path
82
+ #
83
+ # According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an
84
+ # invalid Request-URI should result in a 400 status code, but
85
+ # HandshakeError's currently result in a WebSocket abort. It's not
86
+ # clear which should take precedence, but an abort will do just fine.
87
+ begin
88
+ uri = URI.parse(@parser.request_url)
89
+ @path = uri.path
90
+ @query_string = uri.query || ""
91
+ rescue URI::InvalidURIError
92
+ raise HandshakeError, "Invalid request URI: #{@parser.request_url}"
93
+ end
94
+
76
95
  # Validate Upgrade
77
96
  unless @parser.upgrade?
78
97
  raise HandshakeError, "Not an upgrade request"
79
98
  end
80
99
  upgrade = @headers['upgrade']
81
100
  unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
82
- raise HandshakeError, "Invalid upgrade header: #{upgrade}"
101
+ raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
83
102
  end
84
103
 
85
104
  # Determine version heuristically
@@ -10,11 +10,6 @@ module EventMachine
10
10
  raise HandshakeError, "sec-websocket-key header is required"
11
11
  end
12
12
 
13
- # Optional
14
- origin = headers['sec-websocket-origin']
15
- protocols = headers['sec-websocket-protocol']
16
- extensions = headers['sec-websocket-extensions']
17
-
18
13
  string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
19
14
  signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
20
15
 
@@ -22,12 +17,21 @@ module EventMachine
22
17
  upgrade << "Upgrade: websocket"
23
18
  upgrade << "Connection: Upgrade"
24
19
  upgrade << "Sec-WebSocket-Accept: #{signature}"
20
+ if protocol = headers['sec-websocket-protocol']
21
+ validate_protocol!(protocol)
22
+ upgrade << "Sec-WebSocket-Protocol: #{protocol}"
23
+ end
25
24
 
26
- # TODO: Support sec-websocket-protocol
25
+ # TODO: Support sec-websocket-protocol selection
27
26
  # TODO: sec-websocket-extensions
28
27
 
29
28
  return upgrade.join("\r\n") + "\r\n\r\n"
30
29
  end
30
+
31
+ def self.validate_protocol!(protocol)
32
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
33
+ # TODO: Validate characters
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -9,10 +9,20 @@ module EventMachine
9
9
  upgrade << "Upgrade: WebSocket\r\n"
10
10
  upgrade << "Connection: Upgrade\r\n"
11
11
  upgrade << "WebSocket-Origin: #{headers['origin']}\r\n"
12
- upgrade << "WebSocket-Location: #{location}\r\n\r\n"
12
+ upgrade << "WebSocket-Location: #{location}\r\n"
13
+ if protocol = headers['sec-websocket-protocol']
14
+ validate_protocol!(protocol)
15
+ upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
16
+ end
17
+ upgrade << "\r\n"
13
18
 
14
19
  return upgrade
15
20
  end
21
+
22
+ def self.validate_protocol!(protocol)
23
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
24
+ # TODO: Validate characters
25
+ end
16
26
  end
17
27
  end
18
28
  end
@@ -39,6 +39,10 @@ module EventMachine::WebSocket
39
39
  end
40
40
 
41
41
  def numbers_over_spaces(string)
42
+ unless string
43
+ raise HandshakeError, "WebSocket key1 or key2 is missing"
44
+ end
45
+
42
46
  numbers = string.scan(/[0-9]/).join.to_i
43
47
 
44
48
  spaces = string.scan(/ /).size
@@ -15,12 +15,8 @@ module EventMachine
15
15
  @masking_key = nil
16
16
  end
17
17
 
18
- def slice_mask
19
- slice!(0, 4)
20
- end
21
-
22
18
  def getbyte(index)
23
- if @masking_key
19
+ if defined?(@masking_key) && @masking_key
24
20
  masked_char = super
25
21
  masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
26
22
  else
@@ -6,17 +6,20 @@ module EventMachine
6
6
  def message(message_type, extension_data, application_data)
7
7
  case message_type
8
8
  when :close
9
+ @close_info = {
10
+ :code => 1005,
11
+ :reason => "",
12
+ :was_clean => true,
13
+ }
9
14
  if @state == :closing
10
15
  # TODO: Check that message body matches sent data
11
16
  # We can close connection immediately since there is no more data
12
17
  # is allowed to be sent or received on this connection
13
18
  @connection.close_connection
14
- @state = :closed
15
19
  else
16
20
  # Acknowlege close
17
21
  # The connection is considered closed
18
22
  send_frame(:close, application_data)
19
- @state = :closed
20
23
  @connection.close_connection_after_writing
21
24
  end
22
25
  when :ping
@@ -31,7 +34,7 @@ module EventMachine
31
34
  end
32
35
  @connection.trigger_on_message(application_data)
33
36
  when :binary
34
- @connection.trigger_on_message(application_data)
37
+ @connection.trigger_on_binary(application_data)
35
38
  end
36
39
  end
37
40
 
@@ -19,32 +19,53 @@ module EventMachine
19
19
 
20
20
  debug [:close_frame_received, status_code, application_data]
21
21
 
22
+ @close_info = {
23
+ :code => status_code || 1005,
24
+ :reason => application_data,
25
+ :was_clean => true,
26
+ }
27
+
22
28
  if @state == :closing
23
29
  # We can close connection immediately since no more data may be
24
30
  # sent or received on this connection
25
31
  @connection.close_connection
26
- @state = :closed
27
- else
28
- # Acknowlege close
32
+ elsif @state == :connected
33
+ # Acknowlege close & echo status back to client
29
34
  # The connection is considered closed
30
- send_frame(:close, '')
31
- @state = :closed
35
+ close_data = [status_code || 1000].pack('n')
36
+ send_frame(:close, close_data)
32
37
  @connection.close_connection_after_writing
33
- # TODO: Send close status code and body to app code
34
38
  end
35
39
  when :ping
36
- # Pong back the same data
37
- send_frame(:pong, application_data)
40
+ # There are a couple of protections here against malicious/broken WebSocket abusing ping frames.
41
+ #
42
+ # 1. Delay 200ms before replying. This reduces the number of pings from WebSocket clients behaving as
43
+ # `for (;;) { send_ping(conn); rcv_pong(conn); }`. The spec says we "SHOULD respond with Pong frame as soon
44
+ # as is practical".
45
+ # 2. Reply at most every 200ms. This reduces the number of pong frames sent to WebSocket clients behaving as
46
+ # `for (;;) { send_ping(conn); }`. The spec says "If an endpoint receives a Ping frame and has not yet sent
47
+ # Pong frame(s) in response to previous Ping frame(s), the endpoint MAY elect to send a Pong frame for only
48
+ # the most recently processed Ping frame."
49
+ @most_recent_pong_application_data = application_data
50
+ if @pong_timer == nil then
51
+ @pong_timer = EventMachine.add_timer(0.2) do
52
+ @pong_timer = nil
53
+ send_frame(:pong, @most_recent_pong_application_data)
54
+ end
55
+ end
38
56
  @connection.trigger_on_ping(application_data)
39
57
  when :pong
40
58
  @connection.trigger_on_pong(application_data)
41
59
  when :text
42
60
  if application_data.respond_to?(:force_encoding)
43
61
  application_data.force_encoding("UTF-8")
62
+ unless application_data.valid_encoding?
63
+ raise InvalidDataError, "Invalid UTF8 data"
64
+ end
44
65
  end
45
66
  @connection.trigger_on_message(application_data)
46
67
  when :binary
47
- @connection.trigger_on_message(application_data)
68
+ @connection.trigger_on_binary(application_data)
48
69
  end
49
70
  end
50
71
 
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Websocket
3
- VERSION = "0.4.0"
3
+ VERSION = "0.5.3"
4
4
  end
5
5
  end
@@ -2,8 +2,11 @@ module EventMachine
2
2
  module WebSocket
3
3
  class << self
4
4
  attr_accessor :max_frame_size
5
+ attr_accessor :close_timeout
5
6
  end
6
7
  @max_frame_size = 10 * 1024 * 1024 # 10MB
8
+ # Connections are given 60s to close after being sent a close handshake
9
+ @close_timeout = 60
7
10
 
8
11
  # All errors raised by em-websocket should descend from this class
9
12
  class WebSocketError < RuntimeError; end
@@ -17,6 +20,10 @@ module EventMachine
17
20
  def code; 1002; end
18
21
  end
19
22
 
23
+ class InvalidDataError < WSProtocolError
24
+ def code; 1007; end
25
+ end
26
+
20
27
  # 1009: Message too big to process
21
28
  class WSMessageTooBigError < WSProtocolError
22
29
  def code; 1009; end
data/spec/helper.rb CHANGED
@@ -1,10 +1,15 @@
1
+ # encoding: BINARY
2
+
1
3
  require 'rubygems'
2
4
  require 'rspec'
3
5
  require 'em-spec/rspec'
4
- require 'pp'
5
6
  require 'em-http'
6
7
 
7
8
  require 'em-websocket'
9
+ require 'em-websocket-client'
10
+
11
+ require 'integration/shared_examples'
12
+ require 'integration/gte_03_examples'
8
13
 
9
14
  RSpec.configure do |c|
10
15
  c.mock_with :rspec
@@ -27,75 +32,80 @@ class FakeWebSocketClient < EM::Connection
27
32
  # puts "RECEIVE DATA #{data}"
28
33
  if @state == :new
29
34
  @handshake_response = data
30
- @onopen.call if @onopen
35
+ @onopen.call if defined? @onopen
31
36
  @state = :open
32
37
  else
33
- @onmessage.call(data) if @onmessage
38
+ @onmessage.call(data) if defined? @onmessage
34
39
  @packets << data
35
40
  end
36
41
  end
37
42
 
38
- def send(data)
39
- send_data("\x00#{data}\xff")
43
+ def send(application_data)
44
+ send_frame(:text, application_data)
45
+ end
46
+
47
+ def send_frame(type, application_data)
48
+ send_data construct_frame(type, application_data)
40
49
  end
41
50
 
42
51
  def unbind
43
- @onclose.call if @onclose
52
+ @onclose.call if defined? @onclose
53
+ end
54
+
55
+ private
56
+
57
+ def construct_frame(type, data)
58
+ "\x00#{data}\xff"
44
59
  end
45
60
  end
46
61
 
47
62
  class Draft03FakeWebSocketClient < FakeWebSocketClient
48
- def send(application_data)
49
- frame = ''
50
- opcode = 4 # fake only supports text frames
51
- byte1 = opcode # since more, rsv1-3 are 0
52
- frame << byte1
63
+ private
53
64
 
54
- length = application_data.size
65
+ def construct_frame(type, data)
66
+ frame = ""
67
+ frame << EM::WebSocket::Framing03::FRAME_TYPES[type]
68
+ frame << encoded_length(data.size)
69
+ frame << data
70
+ end
71
+
72
+ def encoded_length(length)
55
73
  if length <= 125
56
- byte2 = length # since rsv4 is 0
57
- frame << byte2
74
+ [length].pack('C') # since rsv4 is 0
58
75
  elsif length < 65536 # write 2 byte length
59
- frame << 126
60
- frame << [length].pack('n')
76
+ "\126#{[length].pack('n')}"
61
77
  else # write 8 byte length
62
- frame << 127
63
- frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
78
+ "\127#{[length >> 32, length & 0xFFFFFFFF].pack("NN")}"
64
79
  end
65
-
66
- frame << application_data
67
-
68
- send_data(frame)
69
80
  end
70
81
  end
71
82
 
72
- class Draft07FakeWebSocketClient < FakeWebSocketClient
73
- def send(application_data)
74
- frame = ''
75
- opcode = 1 # fake only supports text frames
76
- byte1 = opcode | 0b10000000 # since more, rsv1-3 are 0
77
- frame << byte1
83
+ class Draft05FakeWebSocketClient < Draft03FakeWebSocketClient
84
+ private
78
85
 
79
- length = application_data.size
80
- if length <= 125
81
- byte2 = length # since rsv4 is 0
82
- frame << byte2
83
- elsif length < 65536 # write 2 byte length
84
- frame << 126
85
- frame << [length].pack('n')
86
- else # write 8 byte length
87
- frame << 127
88
- frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
89
- end
86
+ def construct_frame(type, data)
87
+ frame = ""
88
+ frame << "\x00\x00\x00\x00" # Mask with nothing for simplicity
89
+ frame << (EM::WebSocket::Framing05::FRAME_TYPES[type] | 0b10000000)
90
+ frame << encoded_length(data.size)
91
+ frame << data
92
+ end
93
+ end
90
94
 
91
- frame << application_data
95
+ class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient
96
+ private
92
97
 
93
- send_data(frame)
98
+ def construct_frame(type, data)
99
+ frame = ""
100
+ frame << (EM::WebSocket::Framing07::FRAME_TYPES[type] | 0b10000000)
101
+ # Should probably mask the data, but I get away without bothering since
102
+ # the server doesn't enforce that incoming frames are masked
103
+ frame << encoded_length(data.size)
104
+ frame << data
94
105
  end
95
106
  end
96
107
 
97
-
98
- # Wrap EM:HttpRequest in a websocket like interface so that it can be used in the specs with the same interface as FakeWebSocketClient
108
+ # Wrapper around em-websocket-client
99
109
  class Draft75WebSocketClient
100
110
  def onopen(&blk); @onopen = blk; end
101
111
  def onclose(&blk); @onclose = blk; end
@@ -103,18 +113,17 @@ class Draft75WebSocketClient
103
113
  def onmessage(&blk); @onmessage = blk; end
104
114
 
105
115
  def initialize
106
- @ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get({
107
- :timeout => 0,
108
- :origin => 'http://example.com',
109
- })
110
- @ws.errback { @onerror.call if @onerror }
111
- @ws.callback { @onopen.call if @onopen }
112
- @ws.stream { |msg| @onmessage.call(msg) if @onmessage }
113
- @ws.disconnect { @onclose.call if @onclose }
116
+ @ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/',
117
+ :version => 75,
118
+ :origin => 'http://example.com')
119
+ @ws.errback { |err| @onerror.call if defined? @onerror }
120
+ @ws.callback { @onopen.call if defined? @onopen }
121
+ @ws.stream { |msg| @onmessage.call(msg) if defined? @onmessage }
122
+ @ws.disconnect { @onclose.call if defined? @onclose }
114
123
  end
115
124
 
116
125
  def send(message)
117
- @ws.send(message)
126
+ @ws.send_msg(message)
118
127
  end
119
128
 
120
129
  def close_connection
@@ -122,6 +131,12 @@ class Draft75WebSocketClient
122
131
  end
123
132
  end
124
133
 
134
+ def start_server(opts = {})
135
+ EM::WebSocket.run({:host => "0.0.0.0", :port => 12345}.merge(opts)) { |ws|
136
+ yield ws if block_given?
137
+ }
138
+ end
139
+
125
140
  def format_request(r)
126
141
  data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
127
142
  header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
@@ -14,32 +14,29 @@ describe "WebSocket server" do
14
14
  http.callback { fail }
15
15
  end
16
16
 
17
- EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) {}
17
+ start_server
18
18
  }
19
19
  end
20
20
 
21
21
  it "should expose the WebSocket request headers, path and query params" do
22
22
  em {
23
23
  EM.add_timer(0.1) do
24
- http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
25
- http.errback { fail }
26
- http.callback {
27
- http.response_header.status.should == 101
28
- http.close_connection
29
- }
30
- http.stream { |msg| }
24
+ ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/',
25
+ :origin => 'http://example.com')
26
+ ws.errback { fail }
27
+ ws.callback { ws.close_connection }
28
+ ws.stream { |msg| }
31
29
  end
32
30
 
33
- EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
31
+ start_server do |ws|
34
32
  ws.onopen { |handshake|
35
33
  headers = handshake.headers
36
- headers["User-Agent"].should == "EventMachine HttpClient"
37
34
  headers["Connection"].should == "Upgrade"
38
- headers["Upgrade"].should == "WebSocket"
35
+ headers["Upgrade"].should == "websocket"
39
36
  headers["Host"].to_s.should == "127.0.0.1:12345"
40
37
  handshake.path.should == "/"
41
38
  handshake.query.should == {}
42
- handshake.origin.should == "127.0.0.1"
39
+ handshake.origin.should == 'http://example.com'
43
40
  }
44
41
  ws.onclose {
45
42
  ws.state.should == :closed
@@ -52,19 +49,15 @@ describe "WebSocket server" do
52
49
  it "should expose the WebSocket path and query params when nonempty" do
53
50
  em {
54
51
  EM.add_timer(0.1) do
55
- http = EM::HttpRequest.new('ws://127.0.0.1:12345/hello').get({
56
- :query => {'foo' => 'bar', 'baz' => 'qux'},
57
- :timeout => 0
58
- })
59
- http.errback { fail }
60
- http.callback {
61
- http.response_header.status.should == 101
62
- http.close_connection
52
+ ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/hello?foo=bar&baz=qux')
53
+ ws.errback { fail }
54
+ ws.callback {
55
+ ws.close_connection
63
56
  }
64
- http.stream { |msg| }
57
+ ws.stream { |msg| }
65
58
  end
66
59
 
67
- EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
60
+ start_server do |ws|
68
61
  ws.onopen { |handshake|
69
62
  handshake.path.should == '/hello'
70
63
  handshake.query_string.split('&').sort.
@@ -82,7 +75,7 @@ describe "WebSocket server" do
82
75
  it "should raise an exception if frame sent before handshake complete" do
83
76
  em {
84
77
  # 1. Start WebSocket server
85
- EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) { |ws|
78
+ start_server { |ws|
86
79
  # 3. Try to send a message to the socket
87
80
  lambda {
88
81
  ws.send('early message')
@@ -98,18 +91,15 @@ describe "WebSocket server" do
98
91
  it "should allow the server to be started inside an existing EM" do
99
92
  em {
100
93
  EM.add_timer(0.1) do
101
- http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
102
- http.errback { fail }
103
- http.callback {
104
- http.response_header.status.should == 101
105
- http.close_connection
106
- }
107
- http.stream { |msg| }
94
+ http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
95
+ http.errback { |e| done }
96
+ http.callback { fail }
108
97
  end
109
98
 
110
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) do |ws|
99
+ start_server do |ws|
111
100
  ws.onopen { |handshake|
112
- handshake.headers["User-Agent"].should == "EventMachine HttpClient"
101
+ headers = handshake.headers
102
+ headers["Host"].to_s.should == "127.0.0.1:12345"
113
103
  }
114
104
  ws.onclose {
115
105
  ws.state.should == :closed
@@ -118,4 +108,31 @@ describe "WebSocket server" do
118
108
  end
119
109
  }
120
110
  end
111
+
112
+ context "outbound limit set" do
113
+ it "should close the connection if the limit is reached" do
114
+ em {
115
+ start_server(:outbound_limit => 150) do |ws|
116
+ # Increase the message size by one on each loop
117
+ ws.onmessage{|msg| ws.send(msg + "x") }
118
+ ws.onclose{|status|
119
+ status[:code].should == 1006 # Unclean
120
+ status[:was_clean].should be false
121
+ }
122
+ end
123
+
124
+ EM.add_timer(0.1) do
125
+ ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
126
+ ws.callback { ws.send_msg "hello" }
127
+ ws.disconnect { done } # Server closed the connection
128
+ ws.stream { |msg|
129
+ # minus frame size ? (getting 146 max here)
130
+ msg.data.size.should <= 150
131
+ # Return back the message
132
+ ws.send_msg(msg.data)
133
+ }
134
+ end
135
+ }
136
+ end
137
+ end
121
138
  end