em-websocket 0.4.0 → 0.5.3

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.
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