em-websocket 0.3.7 → 0.5.2

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +52 -0
  3. data/Gemfile +6 -0
  4. data/LICENCE +7 -0
  5. data/README.md +105 -40
  6. data/examples/echo.rb +22 -6
  7. data/examples/test.html +5 -6
  8. data/lib/em-websocket.rb +2 -1
  9. data/lib/em-websocket/close03.rb +3 -0
  10. data/lib/em-websocket/close05.rb +3 -0
  11. data/lib/em-websocket/close06.rb +3 -0
  12. data/lib/em-websocket/close75.rb +2 -1
  13. data/lib/em-websocket/connection.rb +154 -48
  14. data/lib/em-websocket/framing03.rb +3 -2
  15. data/lib/em-websocket/framing05.rb +3 -2
  16. data/lib/em-websocket/framing07.rb +16 -4
  17. data/lib/em-websocket/framing76.rb +1 -4
  18. data/lib/em-websocket/handler.rb +61 -15
  19. data/lib/em-websocket/handler03.rb +0 -1
  20. data/lib/em-websocket/handler05.rb +0 -1
  21. data/lib/em-websocket/handler06.rb +0 -1
  22. data/lib/em-websocket/handler07.rb +0 -1
  23. data/lib/em-websocket/handler08.rb +0 -1
  24. data/lib/em-websocket/handler13.rb +0 -1
  25. data/lib/em-websocket/handler76.rb +2 -0
  26. data/lib/em-websocket/handshake.rb +156 -0
  27. data/lib/em-websocket/handshake04.rb +18 -16
  28. data/lib/em-websocket/handshake75.rb +15 -8
  29. data/lib/em-websocket/handshake76.rb +15 -14
  30. data/lib/em-websocket/masking04.rb +3 -6
  31. data/lib/em-websocket/message_processor_03.rb +6 -3
  32. data/lib/em-websocket/message_processor_06.rb +30 -9
  33. data/lib/em-websocket/version.rb +1 -1
  34. data/lib/em-websocket/websocket.rb +24 -15
  35. data/spec/helper.rb +84 -51
  36. data/spec/integration/common_spec.rb +89 -69
  37. data/spec/integration/draft03_spec.rb +84 -56
  38. data/spec/integration/draft05_spec.rb +14 -12
  39. data/spec/integration/draft06_spec.rb +67 -7
  40. data/spec/integration/draft13_spec.rb +30 -19
  41. data/spec/integration/draft75_spec.rb +46 -40
  42. data/spec/integration/draft76_spec.rb +59 -45
  43. data/spec/integration/gte_03_examples.rb +42 -0
  44. data/spec/integration/shared_examples.rb +119 -0
  45. data/spec/unit/framing_spec.rb +24 -4
  46. data/spec/unit/handshake_spec.rb +216 -0
  47. data/spec/unit/masking_spec.rb +2 -0
  48. metadata +32 -86
  49. data/examples/flash_policy_file_server.rb +0 -21
  50. data/examples/js/FABridge.js +0 -604
  51. data/examples/js/WebSocketMain.swf +0 -0
  52. data/examples/js/swfobject.js +0 -4
  53. data/examples/js/web_socket.js +0 -312
  54. data/lib/em-websocket/handler_factory.rb +0 -109
  55. data/spec/unit/handler_spec.rb +0 -159
@@ -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
@@ -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
@@ -29,7 +25,8 @@ module EventMachine
29
25
  end
30
26
 
31
27
  def getbytes(start_index, count)
32
- data = ''.force_encoding('ASCII-8BIT')
28
+ data = ''
29
+ data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
33
30
  count.times do |i|
34
31
  data << getbyte(start_index + i)
35
32
  end
@@ -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.3.7"
3
+ VERSION = "0.5.2"
4
4
  end
5
5
  end
@@ -1,47 +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
+
3
11
  # All errors raised by em-websocket should descend from this class
4
- #
5
12
  class WebSocketError < RuntimeError; end
6
13
 
7
14
  # Used for errors that occur during WebSocket handshake
8
- #
9
15
  class HandshakeError < WebSocketError; end
10
16
 
11
17
  # Used for errors which should cause the connection to close.
12
18
  # See RFC6455 §7.4.1 for a full description of meanings
13
- #
14
19
  class WSProtocolError < WebSocketError
15
20
  def code; 1002; end
16
21
  end
17
22
 
23
+ class InvalidDataError < WSProtocolError
24
+ def code; 1007; end
25
+ end
26
+
18
27
  # 1009: Message too big to process
19
28
  class WSMessageTooBigError < WSProtocolError
20
29
  def code; 1009; end
21
30
  end
22
31
 
32
+ # Start WebSocket server, including starting eventmachine run loop
23
33
  def self.start(options, &blk)
24
34
  EM.epoll
25
- EM.run do
26
-
35
+ EM.run {
27
36
  trap("TERM") { stop }
28
37
  trap("INT") { stop }
29
38
 
30
- EventMachine::start_server(options[:host], options[:port],
31
- EventMachine::WebSocket::Connection, options) do |c|
32
- blk.call(c)
33
- end
39
+ run(options, &blk)
40
+ }
41
+ end
42
+
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
34
48
  end
35
49
  end
36
50
 
37
51
  def self.stop
38
52
  puts "Terminating WebSocket Server"
39
- EventMachine.stop
53
+ EM.stop
40
54
  end
41
-
42
- class << self
43
- attr_accessor :max_frame_size
44
- end
45
- @max_frame_size = 10 * 1024 * 1024 # 10MB
46
55
  end
47
56
  end
@@ -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
64
+
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
53
71
 
54
- length = application_data.size
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,15 +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(:timeout => 0)
107
- @ws.errback { @onerror.call if @onerror }
108
- @ws.callback { @onopen.call if @onopen }
109
- @ws.stream { |msg| @onmessage.call(msg) if @onmessage }
110
- @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 }
111
123
  end
112
124
 
113
125
  def send(message)
114
- @ws.send(message)
126
+ @ws.send_msg(message)
115
127
  end
116
128
 
117
129
  def close_connection
@@ -119,6 +131,12 @@ class Draft75WebSocketClient
119
131
  end
120
132
  end
121
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
+
122
140
  def format_request(r)
123
141
  data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
124
142
  header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
@@ -133,8 +151,23 @@ def format_response(r)
133
151
  data
134
152
  end
135
153
 
136
- RSpec::Matchers.define :send_handshake do |response|
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
162
+ end
163
+
164
+ RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
137
165
  match do |actual|
138
- 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
139
172
  end
140
173
  end