em-websocket 0.5.0 → 0.5.1

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