sonixlabs-em-websocket 0.3.8 → 0.5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +69 -0
  3. data/Gemfile +6 -0
  4. data/LICENCE +7 -0
  5. data/README.md +100 -56
  6. data/README.md.BACKUP.14928.md +195 -0
  7. data/README.md.BASE.14928.md +77 -0
  8. data/README.md.LOCAL.14928.md +98 -0
  9. data/README.md.REMOTE.14928.md +142 -0
  10. data/examples/echo.rb +23 -7
  11. data/examples/ping.rb +24 -0
  12. data/examples/test.html +5 -6
  13. data/lib/em-websocket.rb +4 -2
  14. data/lib/em-websocket/close03.rb +3 -0
  15. data/lib/em-websocket/close05.rb +3 -0
  16. data/lib/em-websocket/close06.rb +3 -0
  17. data/lib/em-websocket/close75.rb +2 -1
  18. data/lib/em-websocket/connection.rb +219 -73
  19. data/lib/em-websocket/framing03.rb +6 -11
  20. data/lib/em-websocket/framing05.rb +6 -11
  21. data/lib/em-websocket/framing07.rb +25 -20
  22. data/lib/em-websocket/framing76.rb +6 -15
  23. data/lib/em-websocket/handler.rb +69 -28
  24. data/lib/em-websocket/handler03.rb +0 -1
  25. data/lib/em-websocket/handler05.rb +0 -1
  26. data/lib/em-websocket/handler06.rb +0 -1
  27. data/lib/em-websocket/handler07.rb +0 -1
  28. data/lib/em-websocket/handler08.rb +0 -1
  29. data/lib/em-websocket/handler13.rb +0 -1
  30. data/lib/em-websocket/handler76.rb +2 -0
  31. data/lib/em-websocket/handshake.rb +156 -0
  32. data/lib/em-websocket/handshake04.rb +18 -56
  33. data/lib/em-websocket/handshake75.rb +15 -8
  34. data/lib/em-websocket/handshake76.rb +15 -14
  35. data/lib/em-websocket/masking04.rb +4 -30
  36. data/lib/em-websocket/message_processor_03.rb +13 -4
  37. data/lib/em-websocket/message_processor_06.rb +25 -13
  38. data/lib/em-websocket/version.rb +1 -1
  39. data/lib/em-websocket/websocket.rb +35 -24
  40. data/spec/helper.rb +82 -55
  41. data/spec/integration/common_spec.rb +90 -70
  42. data/spec/integration/draft03_spec.rb +84 -56
  43. data/spec/integration/draft05_spec.rb +14 -12
  44. data/spec/integration/draft06_spec.rb +66 -9
  45. data/spec/integration/draft13_spec.rb +59 -29
  46. data/spec/integration/draft75_spec.rb +46 -40
  47. data/spec/integration/draft76_spec.rb +113 -109
  48. data/spec/integration/gte_03_examples.rb +42 -0
  49. data/spec/integration/shared_examples.rb +174 -0
  50. data/spec/unit/framing_spec.rb +83 -110
  51. data/spec/unit/handshake_spec.rb +216 -0
  52. data/spec/unit/masking_spec.rb +2 -0
  53. metadata +31 -71
  54. data/examples/flash_policy_file_server.rb +0 -21
  55. data/examples/js/FABridge.js +0 -604
  56. data/examples/js/WebSocketMain.swf +0 -0
  57. data/examples/js/swfobject.js +0 -4
  58. data/examples/js/web_socket.js +0 -312
  59. data/lib/em-websocket/handler_factory.rb +0 -107
  60. data/spec/unit/handler_spec.rb +0 -147
@@ -4,71 +4,33 @@ require 'base64'
4
4
  module EventMachine
5
5
  module WebSocket
6
6
  module Handshake04
7
+ def self.handshake(headers, _, __)
8
+ # Required
9
+ unless key = headers['sec-websocket-key']
10
+ raise HandshakeError, "sec-websocket-key header is required"
11
+ end
7
12
 
8
- def handshake_key_response(key)
9
13
  string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
10
- Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
11
- end
14
+ signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
12
15
 
13
- def handshake_server
14
- # Required
15
- unless key = request['sec-websocket-key']
16
- raise HandshakeError, "Sec-WebSocket-Key header is required"
17
- end
18
-
19
- # Optional
20
- origin = request['sec-websocket-origin']
21
- protocols = request['sec-websocket-protocol']
22
- extensions = request['sec-websocket-extensions']
23
-
24
16
  upgrade = ["HTTP/1.1 101 Switching Protocols"]
25
17
  upgrade << "Upgrade: websocket"
26
18
  upgrade << "Connection: Upgrade"
27
- upgrade << "Sec-WebSocket-Accept: #{handshake_key_response(key)}"
28
-
29
- # TODO: Support Sec-WebSocket-Protocol
30
- # TODO: Sec-WebSocket-Extensions
31
-
32
- [:upgrade_headers, upgrade]
33
-
34
- return upgrade.join("\r\n") + "\r\n\r\n"
35
- end
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
24
+
25
+ # TODO: Support sec-websocket-protocol selection
26
+ # TODO: sec-websocket-extensions
36
27
 
37
- def handshake_client
38
- request = ["GET /websocket HTTP/1.1"]
39
- request << "Host: #{@request[:host]}:#{@request[:port]}"
40
- request << "Connection: keep-alive, Upgrade"
41
- request << "Sec-WebSocket-Version: 8" # TODO: supply version somehow
42
- request << "Sec-WebSocket-Origin: null"
43
- random16 = (0...16).map{rand(255).chr}.join
44
- random16_base64 = Base64.encode64(random16).chomp
45
- @correct_response = handshake_key_response random16_base64
46
- request << "Sec-WebSocket-Key: #{random16_base64}"
47
- request << "Upgrade: websocket"
48
- # TODO: anything else needed? nothing else parsed anyway
49
- return request.join("\r\n") + "\r\n\r\n"
28
+ return upgrade.join("\r\n") + "\r\n\r\n"
50
29
  end
51
30
 
52
- def client_handle_server_handshake_response(data)
53
- header, msg = data.split "\r\n\r\n"
54
- lines = header.split("\r\n")
55
- accept = false
56
- lines.each do |line|
57
- h = /^([^:]+):\s*(.+)$/.match(line)
58
- if !h.nil? and h[1].strip.downcase == "sec-websocket-accept"
59
- accept = (h[2] == @correct_response)
60
- break
61
- end
62
- end
63
- if accept
64
- @state = :connected #TODO - some actual logic would be nice
65
- @connection.trigger_on_open
66
- if msg # handle message bundled in with handshake response
67
- receive_data(msg)
68
- end
69
- else
70
- close_websocket(1002,nil)
71
- end
31
+ def self.validate_protocol!(protocol)
32
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
33
+ # TODO: Validate characters
72
34
  end
73
35
  end
74
36
  end
@@ -1,21 +1,28 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  module Handshake75
4
- def handshake
5
- location = "#{request['host'].scheme}://#{request['host'].host}"
6
- location << ":#{request['host'].port}" if request['host'].port
7
- location << request['path']
4
+ def self.handshake(headers, path, secure)
5
+ scheme = (secure ? "wss" : "ws")
6
+ location = "#{scheme}://#{headers['host']}#{path}"
8
7
 
9
8
  upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
9
  upgrade << "Upgrade: WebSocket\r\n"
11
10
  upgrade << "Connection: Upgrade\r\n"
12
- upgrade << "WebSocket-Origin: #{request['origin']}\r\n"
13
- upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
-
15
- debug [:upgrade_headers, upgrade]
11
+ upgrade << "WebSocket-Origin: #{headers['origin']}\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"
16
18
 
17
19
  return upgrade
18
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
19
26
  end
20
27
  end
21
28
  end
@@ -1,33 +1,30 @@
1
1
  require 'digest/md5'
2
2
 
3
- module EventMachine
4
- module WebSocket
5
- module Handshake76
6
- def handshake
3
+ module EventMachine::WebSocket
4
+ module Handshake76
5
+ class << self
6
+ def handshake(headers, path, secure)
7
7
  challenge_response = solve_challenge(
8
- request['sec-websocket-key1'],
9
- request['sec-websocket-key2'],
10
- request['third-key']
8
+ headers['sec-websocket-key1'],
9
+ headers['sec-websocket-key2'],
10
+ headers['third-key']
11
11
  )
12
12
 
13
- location = "#{request['host'].scheme}://#{request['host'].host}"
14
- location << ":#{request['host'].port}" if request['host'].port
15
- location << request['path']
13
+ scheme = (secure ? "wss" : "ws")
14
+ location = "#{scheme}://#{headers['host']}#{path}"
16
15
 
17
16
  upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
18
17
  upgrade << "Upgrade: WebSocket\r\n"
19
18
  upgrade << "Connection: Upgrade\r\n"
20
19
  upgrade << "Sec-WebSocket-Location: #{location}\r\n"
21
- upgrade << "Sec-WebSocket-Origin: #{request['origin']}\r\n"
22
- if protocol = request['sec-websocket-protocol']
20
+ upgrade << "Sec-WebSocket-Origin: #{headers['origin']}\r\n"
21
+ if protocol = headers['sec-websocket-protocol']
23
22
  validate_protocol!(protocol)
24
23
  upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
25
24
  end
26
25
  upgrade << "\r\n"
27
26
  upgrade << challenge_response
28
27
 
29
- debug [:upgrade_headers, upgrade]
30
-
31
28
  return upgrade
32
29
  end
33
30
 
@@ -42,6 +39,10 @@ module EventMachine
42
39
  end
43
40
 
44
41
  def numbers_over_spaces(string)
42
+ unless string
43
+ raise HandshakeError, "WebSocket key1 or key2 is missing"
44
+ end
45
+
45
46
  numbers = string.scan(/[0-9]/).join.to_i
46
47
 
47
48
  spaces = string.scan(/ /).size
@@ -10,36 +10,13 @@ module EventMachine
10
10
  @masking_key = String.new(self[0..3])
11
11
  end
12
12
 
13
- def self.create_mask
14
- MaskedString.new "rAnD" #TODO make random 4 character string
15
- end
16
-
17
- def self.create_masked_string(original)
18
- masked_string = MaskedString.new
19
- masking_key = self.create_mask
20
- masked_string << masking_key
21
- original.size.times do |i|
22
- char = original.getbyte(i)
23
- masked_string << (char ^ masking_key.getbyte(i%4))
24
- end
25
- if masked_string.respond_to?(:force_encoding)
26
- masked_string.force_encoding("ASCII-8BIT")
27
- end
28
- masked_string.read_mask # get input string
29
- return masked_string
30
- end
31
-
32
13
  # Removes the mask, behaves like a normal string again
33
14
  def unset_mask
34
15
  @masking_key = nil
35
16
  end
36
17
 
37
- def slice_mask
38
- slice!(0, 4)
39
- end
40
-
41
18
  def getbyte(index)
42
- if @masking_key
19
+ if defined?(@masking_key) && @masking_key
43
20
  masked_char = super
44
21
  masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
45
22
  else
@@ -49,12 +26,9 @@ module EventMachine
49
26
 
50
27
  def getbytes(start_index, count)
51
28
  data = ''
52
- if @masking_key
53
- count.times do |i|
54
- data << getbyte(start_index + i)
55
- end
56
- else
57
- data = String.new(self[start_index..start_index+count])
29
+ data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
30
+ count.times do |i|
31
+ data << getbyte(start_index + i)
58
32
  end
59
33
  data
60
34
  end
@@ -6,33 +6,42 @@ 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
23
26
  # Pong back the same data
24
27
  send_frame(:pong, application_data)
28
+ @connection.trigger_on_ping(application_data)
25
29
  when :pong
26
- # TODO: Do something. Complete a deferrable established by a ping?
30
+ @connection.trigger_on_pong(application_data)
27
31
  when :text
28
32
  if application_data.respond_to?(:force_encoding)
29
33
  application_data.force_encoding("UTF-8")
30
34
  end
31
35
  @connection.trigger_on_message(application_data)
32
36
  when :binary
33
- @connection.trigger_on_message(application_data)
37
+ @connection.trigger_on_binary(application_data)
34
38
  end
35
39
  end
40
+
41
+ # Ping & Pong supported
42
+ def pingable?
43
+ true
44
+ end
36
45
  end
37
46
  end
38
47
  end
@@ -12,41 +12,53 @@ module EventMachine
12
12
  nil
13
13
  when 1
14
14
  # Illegal close frame
15
- raise DataError, "Close frames with a body must contain a 2 byte status code"
15
+ raise WSProtocolError, "Close frames with a body must contain a 2 byte status code"
16
16
  else
17
17
  application_data.slice!(0, 2).unpack('n').first
18
18
  end
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
- # We can close connection immediately since there is no more data
24
- # is allowed to be sent or received on this connection
29
+ # We can close connection immediately since no more data may be
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
40
  # Pong back the same data
37
41
  send_frame(:pong, application_data)
42
+ @connection.trigger_on_ping(application_data)
38
43
  when :pong
39
- # TODO: Do something. Complete a deferrable established by a ping?
40
- @connection.trigger_on_message(application_data, :pong)
44
+ @connection.trigger_on_pong(application_data)
41
45
  when :text
42
46
  if application_data.respond_to?(:force_encoding)
43
47
  application_data.force_encoding("UTF-8")
48
+ unless application_data.valid_encoding?
49
+ raise InvalidDataError, "Invalid UTF8 data"
50
+ end
44
51
  end
45
- @connection.trigger_on_message(application_data, :text)
52
+ @connection.trigger_on_message(application_data)
46
53
  when :binary
47
- @connection.trigger_on_message(application_data, :binary)
54
+ @connection.trigger_on_binary(application_data)
48
55
  end
49
56
  end
57
+
58
+ # Ping & Pong supported
59
+ def pingable?
60
+ true
61
+ end
50
62
  end
51
63
  end
52
64
  end
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Websocket
3
- VERSION = "0.3.8"
3
+ VERSION = "0.5.1.1"
4
4
  end
5
5
  end
@@ -1,45 +1,56 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
+ class << self
4
+ attr_accessor :max_frame_size
5
+ attr_accessor :close_timeout
6
+ end
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
10
+
11
+ # All errors raised by em-websocket should descend from this class
3
12
  class WebSocketError < RuntimeError; end
13
+
14
+ # Used for errors that occur during WebSocket handshake
4
15
  class HandshakeError < WebSocketError; end
5
- class DataError < WebSocketError; end
6
16
 
7
- #backwards compatibility
8
- def self.start(options, &blk)
9
- self.start_ws_server(options, &blk)
17
+ # Used for errors which should cause the connection to close.
18
+ # See RFC6455 §7.4.1 for a full description of meanings
19
+ class WSProtocolError < WebSocketError
20
+ def code; 1002; end
10
21
  end
11
22
 
12
- def self.start_ws_server(options, &blk)
13
- EM.epoll
14
- EM.run do
15
-
16
- #trap("TERM") { stop; }
17
- #trap("INT") { stop; }
23
+ class InvalidDataError < WSProtocolError
24
+ def code; 1007; end
25
+ end
18
26
 
19
- EventMachine::start_server(options[:host], options[:port],
20
- EventMachine::WebSocket::Connection, options) do |c|
21
- blk.call(c)
22
- end
23
- end
27
+ # 1009: Message too big to process
28
+ class WSMessageTooBigError < WSProtocolError
29
+ def code; 1009; end
24
30
  end
25
31
 
26
- def self.start_ws_client(options, &blk)
32
+ # Start WebSocket server, including starting eventmachine run loop
33
+ def self.start(options, &blk)
27
34
  EM.epoll
28
- EM.run do
35
+ EM.run {
36
+ trap("TERM") { stop }
37
+ trap("INT") { stop }
29
38
 
30
- #trap("TERM") { stop; raise "TERM" }
31
- #trap("INT") { stop; raise "INT" }
39
+ run(options, &blk)
40
+ }
41
+ end
32
42
 
33
- EM.connect(options[:host], options[:port],
34
- EventMachine::WebSocket::ClientConnection, options) do |c|
35
- blk.call(c)
36
- end
43
+ # Start WebSocket server inside eventmachine run loop
44
+ def self.run(options)
45
+ host, port = options.values_at(:host, :port)
46
+ EM.start_server(host, port, Connection, options) do |c|
47
+ yield c
37
48
  end
38
49
  end
39
50
 
40
51
  def self.stop
41
52
  puts "Terminating WebSocket Server"
42
- EventMachine.stop
53
+ EM.stop
43
54
  end
44
55
  end
45
56
  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,77 +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)
40
45
  end
41
46
 
42
- def unbind
43
- @onclose.call if @onclose
47
+ def send_frame(type, application_data)
48
+ send_data construct_frame(type, application_data)
44
49
  end
45
- end
46
50
 
47
- 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
53
-
54
- length = application_data.size
55
- if length <= 125
56
- byte2 = length # since rsv4 is 0
57
- frame << byte2
58
- elsif length < 65536 # write 2 byte length
59
- frame << 126
60
- frame << [length].pack('n')
61
- else # write 8 byte length
62
- frame << 127
63
- frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
64
- end
51
+ def unbind
52
+ @onclose.call if defined? @onclose
53
+ end
65
54
 
66
- frame << application_data
55
+ private
67
56
 
68
- send_data(frame)
57
+ def construct_frame(type, data)
58
+ "\x00#{data}\xff"
69
59
  end
70
60
  end
71
61
 
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
62
+ class Draft03FakeWebSocketClient < FakeWebSocketClient
63
+ private
78
64
 
79
- mask = 0b10000000
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
80
71
 
81
- length = application_data.size
72
+ def encoded_length(length)
82
73
  if length <= 125
83
- byte2 = length # since rsv4 is 0
84
- frame << (mask | byte2)
74
+ [length].pack('C') # since rsv4 is 0
85
75
  elsif length < 65536 # write 2 byte length
86
- frame << (mask | 126)
87
- frame << [length].pack('n')
76
+ "\126#{[length].pack('n')}"
88
77
  else # write 8 byte length
89
- frame << (mask | 127)
90
- frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
78
+ "\127#{[length >> 32, length & 0xFFFFFFFF].pack("NN")}"
91
79
  end
80
+ end
81
+ end
92
82
 
93
- frame << EventMachine::WebSocket::MaskedString.create_masked_string(application_data)
83
+ class Draft05FakeWebSocketClient < Draft03FakeWebSocketClient
84
+ private
94
85
 
95
- send_data(frame)
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
96
92
  end
97
93
  end
98
94
 
95
+ class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient
96
+ private
99
97
 
100
- # Wrap EM:HttpRequest in a websocket like interface so that it can be used in the specs with the same interface as FakeWebSocketClient
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
105
+ end
106
+ end
107
+
108
+ # Wrapper around em-websocket-client
101
109
  class Draft75WebSocketClient
102
110
  def onopen(&blk); @onopen = blk; end
103
111
  def onclose(&blk); @onclose = blk; end
@@ -105,14 +113,17 @@ class Draft75WebSocketClient
105
113
  def onmessage(&blk); @onmessage = blk; end
106
114
 
107
115
  def initialize
108
- @ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:timeout => 0)
109
- @ws.errback { @onerror.call if @onerror }
110
- @ws.callback { @onopen.call if @onopen }
111
- @ws.stream { |msg| @onmessage.call(msg) if @onmessage }
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 }
112
123
  end
113
124
 
114
125
  def send(message)
115
- @ws.send(message)
126
+ @ws.send_msg(message)
116
127
  end
117
128
 
118
129
  def close_connection
@@ -120,6 +131,12 @@ class Draft75WebSocketClient
120
131
  end
121
132
  end
122
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
+
123
140
  def format_request(r)
124
141
  data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
125
142
  header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
@@ -134,13 +151,23 @@ def format_response(r)
134
151
  data
135
152
  end
136
153
 
137
- def handler(request, secure = false)
138
- connection = Object.new
139
- EM::WebSocket::HandlerFactory.build(connection, format_request(request), secure)
154
+ RSpec::Matchers.define :succeed_with_upgrade do |response|
155
+ match do |actual|
156
+ success = nil
157
+ actual.callback { |upgrade_response, handler_klass|
158
+ success = (upgrade_response.lines.sort == format_response(response).lines.sort)
159
+ }
160
+ success
161
+ end
140
162
  end
141
163
 
142
- RSpec::Matchers.define :send_handshake do |response|
164
+ RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
143
165
  match do |actual|
144
- actual.handshake.lines.sort == format_response(response).lines.sort
166
+ success = nil
167
+ actual.errback { |e|
168
+ success = (e.class == error_klass)
169
+ success &= (e.message == error_message) if error_message
170
+ }
171
+ success
145
172
  end
146
173
  end