em-websocket 0.3.7 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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