sonixlabs-em-websocket 0.3.8 → 0.5.1.1

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