em-websocket 0.5.0 → 0.5.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 623f09b2677a73143c0f625ba3b516a00219103e
4
+ data.tar.gz: 0ebcdd8490811e0b69a64262b2a63daeb8d6aa14
5
+ SHA512:
6
+ metadata.gz: aad00490beaf8c9ae3606dc506d111c7d60c1f83c6b08c93ebbafd9d877f3c7a7ec2273cbb0a3d97f59d7fb749e4a6f157cdc1a53f4bab88c21cb6eb99605435
7
+ data.tar.gz: 002ff0471519223522a43a3e865272af61c033960d4a74506080b551d8b01f3cc73471c4b19d677b7c3f90c76b8022c34aa5fbb0b4a3b348b37102b1a3ad597e
@@ -1,5 +1,24 @@
1
1
  = Changelog
2
2
 
3
+ == 0.5.1 / 2014-04-23
4
+
5
+ - new features:
6
+ - Support for receiving binary messages
7
+
8
+ - changed:
9
+ - Allow additional close codes to be sent by apps
10
+ - Raise better errors on missing Sec-WebSocket-Key2
11
+ - Updated http_parser.rb dependency to 0.6.0
12
+
13
+ - bug fixes:
14
+ - Abort if HTTP request URI is invalid
15
+ - Force close connections that have been sent a close handshake after a timeout
16
+
17
+ - improved spec compliance on:
18
+ - Missing continuation frames
19
+ - Fragmented control frames
20
+ - Close behaviour after protocol errors
21
+
3
22
  == 0.5.0 / 2013-03-05
4
23
 
5
24
  - new features:
data/Gemfile CHANGED
@@ -1,3 +1,9 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gemspec
4
+
5
+ gem "em-websocket-client", git: "git@github.com:movitto/em-websocket-client.git", branch: "expose-websocket-api"
6
+ gem "em-spec", "~> 0.2.6"
7
+ gem "em-http-request", "~> 1.1.1"
8
+ gem "rspec", "~> 2.12.0"
9
+ gem "rake"
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # EM-WebSocket
2
2
 
3
- EventMachine based, async, Ruby WebSocket server. Take a look at examples directory, or check out the blog post below:
3
+ [![Gem Version](https://badge.fury.io/rb/em-websocket.png)](http://rubygems.org/gems/em-websocket)
4
+ [![Analytics](https://ga-beacon.appspot.com/UA-71196-10/em-websocket/readme)](https://github.com/igrigorik/ga-beacon)
4
5
 
5
- * [Ruby & Websockets: TCP for the Web](http://www.igvita.com/2009/12/22/ruby-websockets-tcp-for-the-browser/)
6
+ EventMachine based, async, Ruby WebSocket server. Take a look at examples directory, or check out the blog post: [Ruby & Websockets: TCP for the Web](http://www.igvita.com/2009/12/22/ruby-websockets-tcp-for-the-browser/).
6
7
 
7
8
  ## Simple server example
8
9
 
@@ -5,6 +5,7 @@ module EventMachine
5
5
  # TODO: Ideally send body data and check that it matches in ack
6
6
  send_frame(:close, '')
7
7
  @state = :closing
8
+ start_close_timeout
8
9
  end
9
10
 
10
11
  def supports_close_codes?; false; end
@@ -5,6 +5,7 @@ module EventMachine
5
5
  # TODO: Ideally send body data and check that it matches in ack
6
6
  send_frame(:close, "\x53")
7
7
  @state = :closing
8
+ start_close_timeout
8
9
  end
9
10
 
10
11
  def supports_close_codes?; false; end
@@ -10,6 +10,7 @@ module EventMachine
10
10
  send_frame(:close, '')
11
11
  end
12
12
  @state = :closing
13
+ start_close_timeout
13
14
  end
14
15
 
15
16
  def supports_close_codes?; true; end
@@ -10,12 +10,16 @@ module EventMachine
10
10
  def onclose(&blk); @onclose = blk; end
11
11
  def onerror(&blk); @onerror = blk; end
12
12
  def onmessage(&blk); @onmessage = blk; end
13
+ def onbinary(&blk); @onbinary = blk; end
13
14
  def onping(&blk); @onping = blk; end
14
15
  def onpong(&blk); @onpong = blk; end
15
16
 
16
17
  def trigger_on_message(msg)
17
18
  @onmessage.call(msg) if defined? @onmessage
18
19
  end
20
+ def trigger_on_binary(msg)
21
+ @onbinary.call(msg) if defined? @onbinary
22
+ end
19
23
  def trigger_on_open(handshake)
20
24
  @onopen.call(handshake) if defined? @onopen
21
25
  end
@@ -40,6 +44,7 @@ module EventMachine
40
44
  @secure = options[:secure] || false
41
45
  @secure_proxy = options[:secure_proxy] || false
42
46
  @tls_options = options[:tls_options] || {}
47
+ @close_timeout = options[:close_timeout]
43
48
 
44
49
  @handler = nil
45
50
 
@@ -72,10 +77,6 @@ module EventMachine
72
77
  else
73
78
  dispatch(data)
74
79
  end
75
- rescue WSProtocolError => e
76
- debug [:error, e]
77
- trigger_on_error(e)
78
- close_websocket_private(e.code, e.message)
79
80
  rescue => e
80
81
  debug [:error, e]
81
82
 
@@ -247,6 +248,10 @@ module EventMachine
247
248
  defined?(@max_frame_size) ? @max_frame_size : WebSocket.max_frame_size
248
249
  end
249
250
 
251
+ def close_timeout
252
+ @close_timeout || WebSocket.close_timeout
253
+ end
254
+
250
255
  private
251
256
 
252
257
  # As definited in draft 06 7.2.2, some failures require that the server
@@ -265,13 +270,39 @@ module EventMachine
265
270
  end
266
271
  end
267
272
 
268
- # Accept 1000, 3xxx or 4xxx
273
+ # Allow applications to close with 1000, 1003, 1008, 1011, 3xxx or 4xxx.
274
+ #
275
+ # em-websocket uses a few other codes internally which should not be
276
+ # used by applications
277
+ #
278
+ # Browsers generally allow connections to be closed with code 1000,
279
+ # 3xxx, and 4xxx. em-websocket allows closing with a few other codes
280
+ # which seem reasonable (for discussion see
281
+ # https://github.com/igrigorik/em-websocket/issues/98)
282
+ #
283
+ # Usage from the rfc:
284
+ #
285
+ # 1000 indicates a normal closure
286
+ #
287
+ # 1003 indicates that an endpoint is terminating the connection
288
+ # because it has received a type of data it cannot accept
289
+ #
290
+ # 1008 indicates that an endpoint is terminating the connection because
291
+ # it has received a message that violates its policy
292
+ #
293
+ # 1011 indicates that a server is terminating the connection because it
294
+ # encountered an unexpected condition that prevented it from fulfilling
295
+ # the request
296
+ #
297
+ # Status codes in the range 3000-3999 are reserved for use by libraries,
298
+ # frameworks, and applications
299
+ #
300
+ # Status codes in the range 4000-4999 are reserved for private use and
301
+ # thus can't be registered
269
302
  #
270
- # This is consistent with the spec and what browsers have implemented
271
- # Frameworks should use 3xxx while applications should use 4xxx
272
303
  def acceptable_close_code?(code)
273
304
  case code
274
- when 1000, (3000..4999)
305
+ when 1000, 1003, 1008, 1011, (3000..4999)
275
306
  true
276
307
  else
277
308
  false
@@ -9,7 +9,7 @@ module EventMachine
9
9
  @frame_type = nil
10
10
  end
11
11
 
12
- def process_data(newdata)
12
+ def process_data
13
13
  error = false
14
14
 
15
15
  while !error && @data.size > 1
@@ -9,7 +9,7 @@ module EventMachine
9
9
  @frame_type = nil
10
10
  end
11
11
 
12
- def process_data(newdata)
12
+ def process_data
13
13
  error = false
14
14
 
15
15
  while !error && @data.size > 5 # mask plus first byte present
@@ -10,7 +10,7 @@ module EventMachine
10
10
  @frame_type = nil
11
11
  end
12
12
 
13
- def process_data(newdata)
13
+ def process_data
14
14
  error = false
15
15
 
16
16
  while !error && @data.size >= 2
@@ -87,8 +87,19 @@ module EventMachine
87
87
 
88
88
  frame_type = opcode_to_type(opcode)
89
89
 
90
- if frame_type == :continuation && !@frame_type
91
- raise WSProtocolError, 'Continuation frame not expected'
90
+ if frame_type == :continuation
91
+ if !@frame_type
92
+ raise WSProtocolError, 'Continuation frame not expected'
93
+ end
94
+ else # Not a continuation frame
95
+ if @frame_type && data_frame?(frame_type)
96
+ raise WSProtocolError, "Continuation frame expected"
97
+ end
98
+ end
99
+
100
+ # Validate that control frames are not fragmented
101
+ if !fin && !data_frame?(frame_type)
102
+ raise WSProtocolError, 'Control frames must not be fragmented'
92
103
  end
93
104
 
94
105
  if !fin
@@ -7,7 +7,7 @@ module EventMachine
7
7
  @data = ''
8
8
  end
9
9
 
10
- def process_data(newdata)
10
+ def process_data
11
11
  debug [:message, @data]
12
12
 
13
13
  # This algorithm comes straight from the spec
@@ -71,9 +71,6 @@ module EventMachine
71
71
  raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)"
72
72
  end
73
73
 
74
- # Optimization to avoid calling slice! unnecessarily
75
- error = true and next unless newdata =~ /\xff/
76
-
77
74
  msg = @data.slice!(/\A\x00[^\xff]*\xff/)
78
75
  if msg
79
76
  msg.gsub!(/\A\x00|\xff\z/, '')
@@ -38,27 +38,49 @@ module EventMachine
38
38
  @connection = connection
39
39
  @debug = debug
40
40
  @state = :connected
41
+ @close_timer = nil
41
42
  initialize_framing
42
43
  end
43
44
 
44
45
  def receive_data(data)
45
46
  @data << data
46
- process_data(data)
47
+ process_data
48
+ rescue WSProtocolError => e
49
+ fail_websocket(e)
47
50
  end
48
51
 
49
52
  def close_websocket(code, body)
50
53
  # Implemented in subclass
51
54
  end
52
55
 
56
+ # Used to avoid un-acked and unclosed remaining open indefinitely
57
+ def start_close_timeout
58
+ @close_timer = EM::Timer.new(@connection.close_timeout) {
59
+ @connection.close_connection
60
+ e = WSProtocolError.new("Close handshake un-acked after #{@connection.close_timeout}s, closing tcp connection")
61
+ @connection.trigger_on_error(e)
62
+ }
63
+ end
64
+
65
+ # This corresponds to "Fail the WebSocket Connection" in the spec.
66
+ def fail_websocket(e)
67
+ debug [:error, e]
68
+ close_websocket(e.code, e.message)
69
+ @connection.close_connection_after_writing
70
+ @connection.trigger_on_error(e)
71
+ end
72
+
53
73
  def unbind
54
74
  @state = :closed
55
75
 
76
+ @close_timer.cancel if @close_timer
77
+
56
78
  @close_info = defined?(@close_info) ? @close_info : {
57
79
  :code => 1006,
58
80
  :was_clean => false,
59
81
  }
60
82
 
61
- @connection.trigger_on_close(@close_info )
83
+ @connection.trigger_on_close(@close_info)
62
84
  end
63
85
 
64
86
  def ping
@@ -1,4 +1,5 @@
1
1
  require "http/parser"
2
+ require "uri"
2
3
 
3
4
  module EventMachine
4
5
  module WebSocket
@@ -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
@@ -77,13 +78,27 @@ module EventMachine
77
78
  raise HandshakeError, "Must be GET request"
78
79
  end
79
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
+
80
95
  # Validate Upgrade
81
96
  unless @parser.upgrade?
82
97
  raise HandshakeError, "Not an upgrade request"
83
98
  end
84
99
  upgrade = @headers['upgrade']
85
100
  unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
86
- raise HandshakeError, "Invalid upgrade header: #{upgrade}"
101
+ raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
87
102
  end
88
103
 
89
104
  # Determine version heuristically
@@ -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
@@ -34,7 +34,7 @@ module EventMachine
34
34
  end
35
35
  @connection.trigger_on_message(application_data)
36
36
  when :binary
37
- @connection.trigger_on_message(application_data)
37
+ @connection.trigger_on_binary(application_data)
38
38
  end
39
39
  end
40
40
 
@@ -51,7 +51,7 @@ module EventMachine
51
51
  end
52
52
  @connection.trigger_on_message(application_data)
53
53
  when :binary
54
- @connection.trigger_on_message(application_data)
54
+ @connection.trigger_on_binary(application_data)
55
55
  end
56
56
  end
57
57
 
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Websocket
3
- VERSION = "0.5.0"
3
+ VERSION = "0.5.1"
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
@@ -6,6 +6,10 @@ require 'em-spec/rspec'
6
6
  require 'em-http'
7
7
 
8
8
  require 'em-websocket'
9
+ require 'em-websocket-client'
10
+
11
+ require 'integration/shared_examples'
12
+ require 'integration/gte_03_examples'
9
13
 
10
14
  RSpec.configure do |c|
11
15
  c.mock_with :rspec
@@ -101,7 +105,7 @@ class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient
101
105
  end
102
106
  end
103
107
 
104
- # 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
105
109
  class Draft75WebSocketClient
106
110
  def onopen(&blk); @onopen = blk; end
107
111
  def onclose(&blk); @onclose = blk; end
@@ -109,18 +113,17 @@ class Draft75WebSocketClient
109
113
  def onmessage(&blk); @onmessage = blk; end
110
114
 
111
115
  def initialize
112
- @ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get({
113
- :timeout => 0,
114
- :origin => 'http://example.com',
115
- })
116
- @ws.errback { @onerror.call if defined? @onerror }
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 }
117
120
  @ws.callback { @onopen.call if defined? @onopen }
118
121
  @ws.stream { |msg| @onmessage.call(msg) if defined? @onmessage }
119
122
  @ws.disconnect { @onclose.call if defined? @onclose }
120
123
  end
121
124
 
122
125
  def send(message)
123
- @ws.send(message)
126
+ @ws.send_msg(message)
124
127
  end
125
128
 
126
129
  def close_connection
@@ -128,6 +131,12 @@ class Draft75WebSocketClient
128
131
  end
129
132
  end
130
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
+
131
140
  def format_request(r)
132
141
  data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
133
142
  header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
@@ -6,12 +6,6 @@ describe "WebSocket server" do
6
6
  include EM::SpecHelper
7
7
  default_timeout 1
8
8
 
9
- def start_server
10
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
11
- yield ws if block_given?
12
- }
13
- end
14
-
15
9
  it "should fail on non WebSocket requests" do
16
10
  em {
17
11
  EM.add_timer(0.1) do
@@ -27,25 +21,22 @@ describe "WebSocket server" do
27
21
  it "should expose the WebSocket request headers, path and query params" do
28
22
  em {
29
23
  EM.add_timer(0.1) do
30
- http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
31
- http.errback { fail }
32
- http.callback {
33
- http.response_header.status.should == 101
34
- http.close_connection
35
- }
36
- 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| }
37
29
  end
38
30
 
39
31
  start_server do |ws|
40
32
  ws.onopen { |handshake|
41
33
  headers = handshake.headers
42
- headers["User-Agent"].should == "EventMachine HttpClient"
43
34
  headers["Connection"].should == "Upgrade"
44
- headers["Upgrade"].should == "WebSocket"
35
+ headers["Upgrade"].should == "websocket"
45
36
  headers["Host"].to_s.should == "127.0.0.1:12345"
46
37
  handshake.path.should == "/"
47
38
  handshake.query.should == {}
48
- handshake.origin.should == "127.0.0.1"
39
+ handshake.origin.should == 'http://example.com'
49
40
  }
50
41
  ws.onclose {
51
42
  ws.state.should == :closed
@@ -58,16 +49,12 @@ describe "WebSocket server" do
58
49
  it "should expose the WebSocket path and query params when nonempty" do
59
50
  em {
60
51
  EM.add_timer(0.1) do
61
- http = EM::HttpRequest.new('ws://127.0.0.1:12345/hello').get({
62
- :query => {'foo' => 'bar', 'baz' => 'qux'},
63
- :timeout => 0
64
- })
65
- http.errback { fail }
66
- http.callback {
67
- http.response_header.status.should == 101
68
- 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
69
56
  }
70
- http.stream { |msg| }
57
+ ws.stream { |msg| }
71
58
  end
72
59
 
73
60
  start_server do |ws|
@@ -104,18 +91,15 @@ describe "WebSocket server" do
104
91
  it "should allow the server to be started inside an existing EM" do
105
92
  em {
106
93
  EM.add_timer(0.1) do
107
- http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
108
- http.errback { fail }
109
- http.callback {
110
- http.response_header.status.should == 101
111
- http.close_connection
112
- }
113
- 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 }
114
97
  end
115
98
 
116
99
  start_server do |ws|
117
100
  ws.onopen { |handshake|
118
- handshake.headers["User-Agent"].should == "EventMachine HttpClient"
101
+ headers = handshake.headers
102
+ headers["Host"].to_s.should == "127.0.0.1:12345"
119
103
  }
120
104
  ws.onclose {
121
105
  ws.state.should == :closed
@@ -1,5 +1,4 @@
1
1
  require 'helper'
2
- require 'integration/shared_examples'
3
2
 
4
3
  describe "draft03" do
5
4
  include EM::SpecHelper
@@ -35,12 +34,6 @@ describe "draft03" do
35
34
  }
36
35
  end
37
36
 
38
- def start_server
39
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
40
- yield ws if block_given?
41
- }
42
- end
43
-
44
37
  def start_client
45
38
  client = EM.connect('0.0.0.0', 12345, Draft03FakeWebSocketClient)
46
39
  client.send_data(format_request(@request))
@@ -52,6 +45,10 @@ describe "draft03" do
52
45
  let(:version) { 3 }
53
46
  end
54
47
 
48
+ it_behaves_like "a WebSocket server drafts 3 and above" do
49
+ let(:version) { 3 }
50
+ end
51
+
55
52
  # These examples are straight from the spec
56
53
  # http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6
57
54
  describe "examples from the spec" do
@@ -114,7 +111,8 @@ describe "draft03" do
114
111
  data = "a" * 256
115
112
 
116
113
  start_server { |ws|
117
- ws.onmessage { |msg|
114
+ ws.onbinary { |msg|
115
+ msg.encoding.should == Encoding.find("BINARY") if defined?(Encoding)
118
116
  msg.should == data
119
117
  done
120
118
  }
@@ -134,7 +132,8 @@ describe "draft03" do
134
132
  data = "a" * 65536
135
133
 
136
134
  start_server { |ws|
137
- ws.onmessage { |msg|
135
+ ws.onbinary { |msg|
136
+ msg.encoding.should == Encoding.find("BINARY") if defined?(Encoding)
138
137
  msg.should == data
139
138
  done
140
139
  }
@@ -1,5 +1,4 @@
1
1
  require 'helper'
2
- require 'integration/shared_examples'
3
2
 
4
3
  describe "draft05" do
5
4
  include EM::SpecHelper
@@ -21,12 +20,6 @@ describe "draft05" do
21
20
  }
22
21
  }
23
22
  end
24
-
25
- def start_server
26
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
27
- yield ws if block_given?
28
- }
29
- end
30
23
 
31
24
  def start_client
32
25
  client = EM.connect('0.0.0.0', 12345, Draft05FakeWebSocketClient)
@@ -39,6 +32,10 @@ describe "draft05" do
39
32
  let(:version) { 5 }
40
33
  end
41
34
 
35
+ it_behaves_like "a WebSocket server drafts 3 and above" do
36
+ let(:version) { 5 }
37
+ end
38
+
42
39
  it "should report that close codes are not supported" do
43
40
  em {
44
41
  start_server { |ws|
@@ -1,5 +1,4 @@
1
1
  require 'helper'
2
- require 'integration/shared_examples'
3
2
 
4
3
  describe "draft06" do
5
4
  include EM::SpecHelper
@@ -30,12 +29,6 @@ describe "draft06" do
30
29
  }
31
30
  }
32
31
  end
33
-
34
- def start_server
35
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
36
- yield ws if block_given?
37
- }
38
- end
39
32
 
40
33
  def start_client
41
34
  client = EM.connect('0.0.0.0', 12345, Draft05FakeWebSocketClient)
@@ -48,6 +41,10 @@ describe "draft06" do
48
41
  let(:version) { 6 }
49
42
  end
50
43
 
44
+ it_behaves_like "a WebSocket server drafts 3 and above" do
45
+ let(:version) { 6 }
46
+ end
47
+
51
48
  it "should open connection" do
52
49
  em {
53
50
  start_server { |server|
@@ -1,7 +1,6 @@
1
1
  # encoding: BINARY
2
2
 
3
3
  require 'helper'
4
- require 'integration/shared_examples'
5
4
 
6
5
  describe "draft13" do
7
6
  include EM::SpecHelper
@@ -33,12 +32,6 @@ describe "draft13" do
33
32
  }
34
33
  end
35
34
 
36
- def start_server
37
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
38
- yield ws if block_given?
39
- }
40
- end
41
-
42
35
  def start_client
43
36
  client = EM.connect('0.0.0.0', 12345, Draft07FakeWebSocketClient)
44
37
  client.send_data(format_request(@request))
@@ -50,6 +43,10 @@ describe "draft13" do
50
43
  let(:version) { 13 }
51
44
  end
52
45
 
46
+ it_behaves_like "a WebSocket server drafts 3 and above" do
47
+ let(:version) { 13 }
48
+ end
49
+
53
50
  it "should send back the correct handshake response" do
54
51
  em {
55
52
  start_server
@@ -1,5 +1,4 @@
1
1
  require 'helper'
2
- require 'integration/shared_examples'
3
2
 
4
3
  # These integration tests are older and use a different testing style to the
5
4
  # integration tests for newer drafts. They use EM::HttpRequest which happens
@@ -9,12 +8,6 @@ describe "WebSocket server draft75" do
9
8
  include EM::SpecHelper
10
9
  default_timeout 1
11
10
 
12
- def start_server
13
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
14
- yield ws if block_given?
15
- }
16
- end
17
-
18
11
  def start_client
19
12
  client = Draft75WebSocketClient.new
20
13
  yield client if block_given?
@@ -29,12 +22,12 @@ describe "WebSocket server draft75" do
29
22
  em {
30
23
  MSG = "Hello World!"
31
24
  EventMachine.add_timer(0.1) do
32
- http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
33
- http.errback { fail }
34
- http.callback { http.response_header.status.should == 101 }
25
+ ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
26
+ ws.errback { fail }
27
+ ws.callback { }
35
28
 
36
- http.stream { |msg|
37
- msg.should == MSG
29
+ ws.stream { |msg|
30
+ msg.data.should == MSG
38
31
  EventMachine.stop
39
32
  }
40
33
  end
@@ -53,13 +46,12 @@ describe "WebSocket server draft75" do
53
46
  received = []
54
47
 
55
48
  EventMachine.add_timer(0.1) do
56
- http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
57
- http.errback { fail }
58
- http.stream {|msg|}
59
- http.callback {
60
- http.response_header.status.should == 101
61
- http.send messages[0]
62
- http.send messages[1]
49
+ ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
50
+ ws.errback { fail }
51
+ ws.stream {|msg|}
52
+ ws.callback {
53
+ ws.send_msg messages[0]
54
+ ws.send_msg messages[1]
63
55
  }
64
56
  end
65
57
 
@@ -79,13 +71,12 @@ describe "WebSocket server draft75" do
79
71
  it "should call onclose callback when client closes connection" do
80
72
  em {
81
73
  EventMachine.add_timer(0.1) do
82
- http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
83
- http.errback { fail }
84
- http.callback {
85
- http.response_header.status.should == 101
86
- http.close_connection
74
+ ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
75
+ ws.errback { fail }
76
+ ws.callback {
77
+ ws.close_connection
87
78
  }
88
- http.stream{|msg|}
79
+ ws.stream{|msg|}
89
80
  end
90
81
 
91
82
  start_server { |ws|
@@ -101,8 +92,8 @@ describe "WebSocket server draft75" do
101
92
  it "should call onerror callback with raised exception and close connection on bad handshake" do
102
93
  em {
103
94
  EventMachine.add_timer(0.1) do
104
- http = EventMachine::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
105
- http.errback { http.response_header.status.should == 0 }
95
+ http = EM::HttpRequest.new('http://127.0.0.1:12345/').get
96
+ http.errback { }
106
97
  http.callback { fail }
107
98
  end
108
99
 
@@ -1,7 +1,6 @@
1
1
  # encoding: BINARY
2
2
 
3
3
  require 'helper'
4
- require 'integration/shared_examples'
5
4
 
6
5
  describe "WebSocket server draft76" do
7
6
  include EM::SpecHelper
@@ -35,12 +34,6 @@ describe "WebSocket server draft76" do
35
34
  :body => "8jKS\'y:G*Co,Wxa-"
36
35
  }
37
36
  end
38
-
39
- def start_server
40
- EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) { |ws|
41
- yield ws if block_given?
42
- }
43
- end
44
37
 
45
38
  def start_client
46
39
  client = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
@@ -178,7 +171,7 @@ describe "WebSocket server draft76" do
178
171
  start_server { |server|
179
172
  server.onerror { |error|
180
173
  error.should be_an_instance_of EM::WebSocket::HandshakeError
181
- error.message.should == "Invalid HTTP header: Could not parse data entirely"
174
+ error.message.should == "Invalid HTTP header: Could not parse data entirely (1 != 29)"
182
175
  done
183
176
  }
184
177
  }
@@ -0,0 +1,42 @@
1
+ shared_examples_for "a WebSocket server drafts 3 and above" do
2
+ it "should force close connections after a timeout if close handshake is not sent by the client" do
3
+ em {
4
+ server_onerror_fired = false
5
+ server_onclose_fired = false
6
+ client_got_close_handshake = false
7
+
8
+ start_server(:close_timeout => 0.1) { |ws|
9
+ ws.onopen {
10
+ # 1: Send close handshake to client
11
+ EM.next_tick { ws.close(4999, "Close message") }
12
+ }
13
+
14
+ ws.onerror { |e|
15
+ # 3: Client should receive onerror
16
+ e.class.should == EM::WebSocket::WSProtocolError
17
+ e.message.should == "Close handshake un-acked after 0.1s, closing tcp connection"
18
+ server_onerror_fired = true
19
+ }
20
+
21
+ ws.onclose {
22
+ server_onclose_fired = true
23
+ }
24
+ }
25
+ start_client { |client|
26
+ client.onmessage { |msg|
27
+ # 2: Client does not respond to close handshake (the fake client
28
+ # doesn't understand them at all hence this is in onmessage)
29
+ msg.should =~ /Close message/ if version >= 6
30
+ client_got_close_handshake = true
31
+ }
32
+
33
+ client.onclose {
34
+ server_onerror_fired.should == true
35
+ server_onclose_fired.should == true
36
+ client_got_close_handshake.should == true
37
+ done
38
+ }
39
+ }
40
+ }
41
+ end
42
+ end
@@ -15,7 +15,7 @@ describe EM::WebSocket::Framing03 do
15
15
 
16
16
  def <<(data)
17
17
  @data << data
18
- process_data(data)
18
+ process_data
19
19
  end
20
20
 
21
21
  def debug(*args); end
@@ -140,7 +140,7 @@ describe EM::WebSocket::Framing04 do
140
140
 
141
141
  def <<(data)
142
142
  @data << data
143
- process_data(data)
143
+ process_data
144
144
  end
145
145
 
146
146
  def debug(*args); end
@@ -209,7 +209,7 @@ describe EM::WebSocket::Framing07 do
209
209
 
210
210
  def <<(data)
211
211
  @data << data
212
- process_data(data)
212
+ process_data
213
213
  end
214
214
 
215
215
  def debug(*args); end
@@ -276,5 +276,23 @@ describe EM::WebSocket::Framing07 do
276
276
  @f << "\x00\x02lo"
277
277
  @f << "\x80\x06 world"
278
278
  end
279
+
280
+ it "should raise if non-fin frame is followed by a non-continuation data frame (continuation frame would be expected)" do
281
+ lambda {
282
+ @f << 0b00000001 # Not fin, text
283
+ @f << 0b00000001 # Length 1
284
+ @f << 'f'
285
+ @f << 0b10000001 # fin, text (continutation expected)
286
+ @f << 0b00000001 # Length 1
287
+ @f << 'b'
288
+ }.should raise_error(EM::WebSocket::WebSocketError, 'Continuation frame expected')
289
+ end
290
+
291
+ it "should raise on non-fin control frames (control frames must not be fragmented)" do
292
+ lambda {
293
+ @f << 0b00001010 # Not fin, pong (opcode 10)
294
+ @f << 0b00000000 # Length 1
295
+ }.should raise_error(EM::WebSocket::WebSocketError, 'Control frames must not be fragmented')
296
+ end
279
297
  end
280
298
  end
@@ -126,6 +126,23 @@ describe EM::WebSocket::Handshake do
126
126
  end
127
127
  end
128
128
 
129
+ it "should raise error if Sec-WebSocket-Key1 is missing" do
130
+ @request[:headers].delete("Sec-WebSocket-Key1")
131
+
132
+ # The error message isn't correct since key1 is used to heuristically
133
+ # determine the protocol version in use, however this test at least checks
134
+ # that the handshake does correctly fail
135
+ handshake(@request).
136
+ should fail_with_error(EM::WebSocket::HandshakeError, 'Extra bytes after header')
137
+ end
138
+
139
+ it "should raise error if Sec-WebSocket-Key2 is missing" do
140
+ @request[:headers].delete("Sec-WebSocket-Key2")
141
+
142
+ handshake(@request).
143
+ should fail_with_error(EM::WebSocket::HandshakeError, 'WebSocket key1 or key2 is missing')
144
+ end
145
+
129
146
  it "should raise error if spaces do not divide numbers in Sec-WebSocket-Key* " do
130
147
  @request[:headers]['Sec-WebSocket-Key2'] = '12998 5 Y3 1.P00'
131
148
 
@@ -138,7 +155,7 @@ describe EM::WebSocket::Handshake do
138
155
  handshake.receive_data("\r\n\r\nfoobar")
139
156
 
140
157
  handshake.
141
- should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid HTTP header: Could not parse data entirely')
158
+ should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid HTTP header: Could not parse data entirely (4 != 10)')
142
159
  end
143
160
 
144
161
  # This might seems crazy, but very occasionally we saw multiple "Upgrade:
@@ -190,4 +207,10 @@ describe EM::WebSocket::Handshake do
190
207
 
191
208
  handshake(@request).should succeed_with_upgrade(@response)
192
209
  end
210
+
211
+ it "should fail if the request URI is invalid" do
212
+ @request[:path] = "/%"
213
+ handshake(@request).should \
214
+ fail_with_error(EM::WebSocket::HandshakeError, 'Invalid request URI: /%')
215
+ end
193
216
  end
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-websocket
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
5
- prerelease:
4
+ version: 0.5.1
6
5
  platform: ruby
7
6
  authors:
8
7
  - Ilya Grigorik
@@ -10,120 +9,36 @@ authors:
10
9
  autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2013-03-05 00:00:00.000000000 Z
12
+ date: 2014-04-23 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: eventmachine
17
16
  requirement: !ruby/object:Gem::Requirement
18
- none: false
19
17
  requirements:
20
- - - ! '>='
18
+ - - ">="
21
19
  - !ruby/object:Gem::Version
22
20
  version: 0.12.9
23
21
  type: :runtime
24
22
  prerelease: false
25
23
  version_requirements: !ruby/object:Gem::Requirement
26
- none: false
27
24
  requirements:
28
- - - ! '>='
25
+ - - ">="
29
26
  - !ruby/object:Gem::Version
30
27
  version: 0.12.9
31
28
  - !ruby/object:Gem::Dependency
32
29
  name: http_parser.rb
33
30
  requirement: !ruby/object:Gem::Requirement
34
- none: false
35
31
  requirements:
36
- - - ~>
32
+ - - "~>"
37
33
  - !ruby/object:Gem::Version
38
- version: 0.5.3
34
+ version: 0.6.0
39
35
  type: :runtime
40
36
  prerelease: false
41
37
  version_requirements: !ruby/object:Gem::Requirement
42
- none: false
43
38
  requirements:
44
- - - ~>
39
+ - - "~>"
45
40
  - !ruby/object:Gem::Version
46
- version: 0.5.3
47
- - !ruby/object:Gem::Dependency
48
- name: em-spec
49
- requirement: !ruby/object:Gem::Requirement
50
- none: false
51
- requirements:
52
- - - ~>
53
- - !ruby/object:Gem::Version
54
- version: 0.2.6
55
- type: :development
56
- prerelease: false
57
- version_requirements: !ruby/object:Gem::Requirement
58
- none: false
59
- requirements:
60
- - - ~>
61
- - !ruby/object:Gem::Version
62
- version: 0.2.6
63
- - !ruby/object:Gem::Dependency
64
- name: eventmachine
65
- requirement: !ruby/object:Gem::Requirement
66
- none: false
67
- requirements:
68
- - - ! '>='
69
- - !ruby/object:Gem::Version
70
- version: '0'
71
- type: :development
72
- prerelease: false
73
- version_requirements: !ruby/object:Gem::Requirement
74
- none: false
75
- requirements:
76
- - - ! '>='
77
- - !ruby/object:Gem::Version
78
- version: '0'
79
- - !ruby/object:Gem::Dependency
80
- name: em-http-request
81
- requirement: !ruby/object:Gem::Requirement
82
- none: false
83
- requirements:
84
- - - ~>
85
- - !ruby/object:Gem::Version
86
- version: 0.2.6
87
- type: :development
88
- prerelease: false
89
- version_requirements: !ruby/object:Gem::Requirement
90
- none: false
91
- requirements:
92
- - - ~>
93
- - !ruby/object:Gem::Version
94
- version: 0.2.6
95
- - !ruby/object:Gem::Dependency
96
- name: rspec
97
- requirement: !ruby/object:Gem::Requirement
98
- none: false
99
- requirements:
100
- - - ~>
101
- - !ruby/object:Gem::Version
102
- version: 2.12.0
103
- type: :development
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- none: false
107
- requirements:
108
- - - ~>
109
- - !ruby/object:Gem::Version
110
- version: 2.12.0
111
- - !ruby/object:Gem::Dependency
112
- name: rake
113
- requirement: !ruby/object:Gem::Requirement
114
- none: false
115
- requirements:
116
- - - ! '>='
117
- - !ruby/object:Gem::Version
118
- version: '0'
119
- type: :development
120
- prerelease: false
121
- version_requirements: !ruby/object:Gem::Requirement
122
- none: false
123
- requirements:
124
- - - ! '>='
125
- - !ruby/object:Gem::Version
126
- version: '0'
41
+ version: 0.6.0
127
42
  description: EventMachine based WebSocket server
128
43
  email:
129
44
  - ilya@igvita.com
@@ -132,7 +47,7 @@ executables: []
132
47
  extensions: []
133
48
  extra_rdoc_files: []
134
49
  files:
135
- - .gitignore
50
+ - ".gitignore"
136
51
  - CHANGELOG.rdoc
137
52
  - Gemfile
138
53
  - README.md
@@ -180,39 +95,33 @@ files:
180
95
  - spec/integration/draft13_spec.rb
181
96
  - spec/integration/draft75_spec.rb
182
97
  - spec/integration/draft76_spec.rb
98
+ - spec/integration/gte_03_examples.rb
183
99
  - spec/integration/shared_examples.rb
184
100
  - spec/unit/framing_spec.rb
185
101
  - spec/unit/handshake_spec.rb
186
102
  - spec/unit/masking_spec.rb
187
103
  homepage: http://github.com/igrigorik/em-websocket
188
104
  licenses: []
105
+ metadata: {}
189
106
  post_install_message:
190
107
  rdoc_options: []
191
108
  require_paths:
192
109
  - lib
193
110
  required_ruby_version: !ruby/object:Gem::Requirement
194
- none: false
195
111
  requirements:
196
- - - ! '>='
112
+ - - ">="
197
113
  - !ruby/object:Gem::Version
198
114
  version: '0'
199
- segments:
200
- - 0
201
- hash: 3271699693430314189
202
115
  required_rubygems_version: !ruby/object:Gem::Requirement
203
- none: false
204
116
  requirements:
205
- - - ! '>='
117
+ - - ">="
206
118
  - !ruby/object:Gem::Version
207
119
  version: '0'
208
- segments:
209
- - 0
210
- hash: 3271699693430314189
211
120
  requirements: []
212
121
  rubyforge_project: em-websocket
213
- rubygems_version: 1.8.24
122
+ rubygems_version: 2.2.2
214
123
  signing_key:
215
- specification_version: 3
124
+ specification_version: 4
216
125
  summary: EventMachine based WebSocket server
217
126
  test_files:
218
127
  - spec/helper.rb
@@ -223,6 +132,7 @@ test_files:
223
132
  - spec/integration/draft13_spec.rb
224
133
  - spec/integration/draft75_spec.rb
225
134
  - spec/integration/draft76_spec.rb
135
+ - spec/integration/gte_03_examples.rb
226
136
  - spec/integration/shared_examples.rb
227
137
  - spec/unit/framing_spec.rb
228
138
  - spec/unit/handshake_spec.rb