faye-websocket 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of faye-websocket might be problematic. Click here for more details.

@@ -1,3 +1,10 @@
1
+ === 0.4.0 / 2012-02-13
2
+
3
+ * Add ping() method to server-side WebSocket and EventSource
4
+ * Buffer send() calls until the draft-76 handshake is complete
5
+ * Fix HTTPS problems on Node 0.7
6
+
7
+
1
8
  === 0.3.0 / 2012-01-13
2
9
 
3
10
  * Add support for EventSource connections
@@ -1,5 +1,8 @@
1
1
  = Faye::WebSocket
2
2
 
3
+ * Travis CI build: {<img src="https://secure.travis-ci.org/faye/faye-websocket-ruby.png" />}[http://travis-ci.org/faye/faye-websocket-ruby]
4
+ * Autobahn tests: {server}[http://faye.jcoglan.com/autobahn/servers/], {client}[http://faye.jcoglan.com/autobahn/clients/]
5
+
3
6
  This is a robust, general-purpose WebSocket implementation extracted from the
4
7
  {Faye}[http://faye.jcoglan.com] project. It provides classes for easily building
5
8
  WebSocket servers and clients in Ruby. It does not provide a server itself, but
@@ -72,6 +75,26 @@ appropriate adapters automatically.
72
75
  Faye::WebSocket.load_adapter('thin')
73
76
  run App
74
77
 
78
+ Note that under certain circumstances (notably a draft-76 client connecting
79
+ through an HTTP proxy), the WebSocket handshake will not be complete after you
80
+ call `Faye::WebSocket.new` because the server will not have received the entire
81
+ handshake from the client yet. In this case, calls to `ws.send` will buffer the
82
+ message in memory until the handshake is complete, at which point any buffered
83
+ messages will be sent to the client.
84
+
85
+ If you need to detect when the WebSocket handshake is complete, you can use the
86
+ `onopen` event.
87
+
88
+ If the connection's protocol version supports it, you can call <tt>ws.ping()</tt>
89
+ to send a ping message and wait for the client's response. This method takes a
90
+ message string, and an optional callback that fires when a matching pong message
91
+ is received. It returns +true+ iff a ping message was sent. If the client does
92
+ not support ping/pong, this method sends no data and returns +false+.
93
+
94
+ ws.ping 'Mic check, one, two' do
95
+ # fires when pong is received
96
+ end
97
+
75
98
 
76
99
  == Using the WebSocket client
77
100
 
@@ -206,6 +229,10 @@ retryable every 10 seconds if the connection is broken:
206
229
 
207
230
  es = Faye::EventSource.new(es, :ping => 15, :retry => 10)
208
231
 
232
+ You can send a ping message at any time by calling <tt>es.ping()</tt>. Unlike
233
+ WebSocket the client does not send a response to this; it is merely to send some
234
+ data over the wire to keep the connection alive.
235
+
209
236
 
210
237
  == Running your socket application
211
238
 
@@ -5,7 +5,7 @@ static = Rack::File.new(File.dirname(__FILE__))
5
5
 
6
6
  App = lambda do |env|
7
7
  if Faye::WebSocket.websocket?(env)
8
- ws = Faye::WebSocket.new(env, ['irc', 'xmpp'])
8
+ ws = Faye::WebSocket.new(env, ['irc', 'xmpp'], :ping => 5)
9
9
  p [:open, ws.url, ws.version, ws.protocol]
10
10
 
11
11
  ws.onmessage = lambda do |event|
@@ -2,11 +2,9 @@ require File.expand_path('../websocket', __FILE__) unless defined?(Faye::WebSock
2
2
 
3
3
  module Faye
4
4
  class EventSource
5
- DEFAULT_PING = 10
6
5
  DEFAULT_RETRY = 5
7
6
 
8
- include WebSocket::API::EventTarget
9
- include WebSocket::API::ReadyStates
7
+ include WebSocket::API
10
8
  attr_reader :env, :url, :ready_state
11
9
 
12
10
  def self.eventsource?(env)
@@ -27,12 +25,14 @@ module Faye
27
25
 
28
26
  def initialize(env, options = {})
29
27
  @env = env
30
- @ping = options[:ping] || DEFAULT_PING
28
+ @ping = options[:ping]
31
29
  @retry = (options[:retry] || DEFAULT_RETRY).to_f
32
30
  @url = EventSource.determine_url(env)
33
31
  @stream = Stream.new(self)
34
32
 
35
33
  @ready_state = CONNECTING
34
+ @send_buffer = []
35
+ EventMachine.next_tick { open }
36
36
 
37
37
  callback = @env['async.callback']
38
38
  callback.call([101, {}, @stream])
@@ -43,11 +43,11 @@ module Faye
43
43
  "\r\n\r\n" +
44
44
  "retry: #{ (@retry * 1000).floor }\r\n\r\n")
45
45
 
46
- @ping_timer = EventMachine.add_periodic_timer(@ping) do
47
- @stream.write(":\r\n\r\n")
48
- end
49
-
50
46
  @ready_state = OPEN
47
+
48
+ if @ping
49
+ @ping_timer = EventMachine.add_periodic_timer(@ping) { ping }
50
+ end
51
51
  end
52
52
 
53
53
  def last_event_id
@@ -73,6 +73,11 @@ module Faye
73
73
  true
74
74
  end
75
75
 
76
+ def ping(message = nil)
77
+ @stream.write(":\r\n\r\n")
78
+ true
79
+ end
80
+
76
81
  def close
77
82
  return if [CLOSING, CLOSED].include?(@ready_state)
78
83
  @ready_state = CLOSED
@@ -91,9 +91,11 @@ module Faye
91
91
  attr_reader :env
92
92
  include API
93
93
 
94
- def initialize(env, supported_protos = nil)
95
- @env = env
96
- @stream = Stream.new(self)
94
+ def initialize(env, supported_protos = nil, options = {})
95
+ @env = env
96
+ @stream = Stream.new(self)
97
+ @ping = options[:ping]
98
+ @ping_id = 0
97
99
 
98
100
  @url = WebSocket.determine_url(@env)
99
101
  @ready_state = CONNECTING
@@ -101,15 +103,26 @@ module Faye
101
103
 
102
104
  @parser = WebSocket.parser(@env).new(self, :protocols => supported_protos)
103
105
 
106
+ @send_buffer = []
107
+ EventMachine.next_tick { open }
108
+
104
109
  @callback = @env['async.callback']
105
110
  @callback.call([101, {}, @stream])
106
111
  @stream.write(@parser.handshake_response)
107
112
 
108
- @ready_state = OPEN
113
+ @ready_state = OPEN if @parser.open?
109
114
 
110
- event = Event.new('open')
111
- event.init_event('open', false, false)
112
- dispatch_event(event)
115
+ if @ping
116
+ @ping_timer = EventMachine.add_periodic_timer(@ping) do
117
+ @ping_id += 1
118
+ ping(@ping_id.to_s)
119
+ end
120
+ end
121
+ end
122
+
123
+ def ping(message = '', &callback)
124
+ return false unless @parser.respond_to?(:ping)
125
+ @parser.ping(message, &callback)
113
126
  end
114
127
 
115
128
  def protocol
@@ -124,7 +137,9 @@ module Faye
124
137
 
125
138
  def parse(data)
126
139
  response = @parser.parse(data)
127
- @stream.write(response) if response
140
+ return unless response
141
+ @stream.write(response)
142
+ open
128
143
  end
129
144
  end
130
145
 
@@ -9,6 +9,9 @@ module Faye
9
9
  CLOSED = 3
10
10
  end
11
11
 
12
+ class IllegalStateError < StandardError
13
+ end
14
+
12
15
  require File.expand_path('../api/event_target', __FILE__)
13
16
  require File.expand_path('../api/event', __FILE__)
14
17
  include EventTarget
@@ -16,8 +19,26 @@ module Faye
16
19
 
17
20
  attr_reader :url, :ready_state, :buffered_amount
18
21
 
22
+ private
23
+
24
+ def open
25
+ return if @parser and not @parser.open?
26
+ @ready_state = OPEN
27
+
28
+ buffer = @send_buffer || []
29
+ while message = buffer.shift
30
+ send(*message)
31
+ end
32
+
33
+ event = Event.new('open')
34
+ event.init_event('open', false, false)
35
+ dispatch_event(event)
36
+ end
37
+
38
+ public
39
+
19
40
  def receive(data)
20
- return false unless ready_state == OPEN
41
+ return false unless @ready_state == OPEN
21
42
  event = Event.new('message')
22
43
  event.init_event('message', false, false)
23
44
  event.data = data
@@ -25,19 +46,30 @@ module Faye
25
46
  end
26
47
 
27
48
  def send(data, type = nil, error_type = nil)
28
- return false if ready_state == CLOSED
49
+ if @ready_state == CONNECTING
50
+ if @send_buffer
51
+ @send_buffer << [data, type, error_type]
52
+ return true
53
+ else
54
+ raise IllegalStateError, 'Cannot call send(), socket is not open yet'
55
+ end
56
+ end
57
+
58
+ return false if @ready_state == CLOSED
59
+
29
60
  data = WebSocket.encode(data) if String === data
30
61
  frame = @parser.frame(data, type, error_type)
31
62
  @stream.write(frame) if frame
32
63
  end
33
64
 
34
65
  def close(code = nil, reason = nil, ack = true)
35
- return if [CLOSING, CLOSED].include?(ready_state)
66
+ return if [CLOSING, CLOSED].include?(@ready_state)
36
67
 
37
68
  @ready_state = CLOSING
38
69
 
39
70
  close = lambda do
40
71
  @ready_state = CLOSED
72
+ EventMachine.cancel_timer(@ping_timer) if @ping_timer
41
73
  @stream.close_connection_after_writing
42
74
  event = Event.new('close', :code => code || 1000, :reason => reason || '')
43
75
  event.init_event('close', false, false)
@@ -23,6 +23,10 @@ module Faye
23
23
  upgrade
24
24
  end
25
25
 
26
+ def open?
27
+ true
28
+ end
29
+
26
30
  def parse(buffer)
27
31
  buffer.each_byte do |data|
28
32
  case @stage
@@ -22,8 +22,6 @@ module Faye
22
22
 
23
23
  def handshake_signature(head)
24
24
  return nil if head.empty?
25
- @handshake_complete = true
26
-
27
25
  env = @socket.env
28
26
 
29
27
  key1 = env['HTTP_SEC_WEBSOCKET_KEY1']
@@ -32,11 +30,17 @@ module Faye
32
30
  key2 = env['HTTP_SEC_WEBSOCKET_KEY2']
33
31
  value2 = number_from_key(key2) / spaces_in_key(key2)
34
32
 
33
+ @handshake_complete = true
34
+
35
35
  Digest::MD5.digest(big_endian(value1) +
36
36
  big_endian(value2) +
37
37
  head)
38
38
  end
39
39
 
40
+ def open?
41
+ !!@handshake_complete
42
+ end
43
+
40
44
  def parse(data)
41
45
  return super if @handshake_complete
42
46
  handshake_signature(data)
@@ -48,8 +48,9 @@ module Faye
48
48
  @stage = 0
49
49
  @masking = options[:masking]
50
50
  @protocols = options[:protocols]
51
-
52
51
  @protocols = @protocols.split(/\s*,\s*/) if String === @protocols
52
+
53
+ @ping_callbacks = {}
53
54
  end
54
55
 
55
56
  def version
@@ -87,7 +88,11 @@ module Faye
87
88
  def create_handshake
88
89
  Handshake.new(@socket.uri, @protocols)
89
90
  end
90
-
91
+
92
+ def open?
93
+ true
94
+ end
95
+
91
96
  def parse(data)
92
97
  @reader.put(data.bytes.to_a)
93
98
  buffer = true
@@ -172,6 +177,11 @@ module Faye
172
177
 
173
178
  WebSocket.encode(frame)
174
179
  end
180
+
181
+ def ping(message = '', &callback)
182
+ @ping_callbacks[message] = callback if callback
183
+ @socket.send(message, :ping)
184
+ end
175
185
 
176
186
  def close(code = nil, reason = nil, &callback)
177
187
  return if @closed
@@ -283,6 +293,12 @@ module Faye
283
293
  when OPCODES[:ping] then
284
294
  return @socket.close(ERRORS[:protocol_error], nil, false) if payload.size > 125
285
295
  @socket.send(payload, :pong)
296
+
297
+ when OPCODES[:pong] then
298
+ message = WebSocket.encode(payload, true)
299
+ callback = @ping_callbacks[message]
300
+ @ping_callbacks.delete(message)
301
+ callback.call if callback
286
302
  end
287
303
  end
288
304
 
@@ -0,0 +1,48 @@
1
+ # encoding=utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "draft-75 parser" do
6
+ it "parses text frames" do
7
+ @web_socket.should_receive(:receive).with("Hello")
8
+ parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
9
+ end
10
+
11
+ it "parses multiple frames from the same packet" do
12
+ @web_socket.should_receive(:receive).with("Hello").exactly(2)
13
+ parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
14
+ end
15
+
16
+ it "parses text frames beginning 0x00-0x7F" do
17
+ @web_socket.should_receive(:receive).with("Hello")
18
+ parse [0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
19
+ end
20
+
21
+ it "ignores frames with a length header" do
22
+ @web_socket.should_not_receive(:receive)
23
+ parse [0x80, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
24
+ end
25
+
26
+ it "parses text following an ignored block" do
27
+ @web_socket.should_receive(:receive).with("Hello")
28
+ parse [0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
29
+ end
30
+
31
+ it "parses multibyte text frames" do
32
+ @web_socket.should_receive(:receive).with(encode "Apple = ")
33
+ parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
34
+ end
35
+
36
+ it "parses frames received in several packets" do
37
+ @web_socket.should_receive(:receive).with(encode "Apple = ")
38
+ parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65]
39
+ parse [0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
40
+ end
41
+
42
+ it "parses fragmented frames" do
43
+ @web_socket.should_receive(:receive).with("Hello")
44
+ parse [0x00, 0x48, 0x65, 0x6c]
45
+ parse [0x6c, 0x6f, 0xff]
46
+ end
47
+ end
48
+
@@ -11,50 +11,6 @@ describe Faye::WebSocket::Draft75Parser do
11
11
  end
12
12
 
13
13
  describe :parse do
14
- shared_examples_for "draft-75 parser" do
15
- it "parses text frames" do
16
- @web_socket.should_receive(:receive).with("Hello")
17
- parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
18
- end
19
-
20
- it "parses multiple frames from the same packet" do
21
- @web_socket.should_receive(:receive).with("Hello").exactly(2)
22
- parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
23
- end
24
-
25
- it "parses text frames beginning 0x00-0x7F" do
26
- @web_socket.should_receive(:receive).with("Hello")
27
- parse [0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
28
- end
29
-
30
- it "ignores frames with a length header" do
31
- @web_socket.should_not_receive(:receive)
32
- parse [0x80, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
33
- end
34
-
35
- it "parses text following an ignored block" do
36
- @web_socket.should_receive(:receive).with("Hello")
37
- parse [0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
38
- end
39
-
40
- it "parses multibyte text frames" do
41
- @web_socket.should_receive(:receive).with(encode "Apple = ")
42
- parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
43
- end
44
-
45
- it "parses frames received in several packets" do
46
- @web_socket.should_receive(:receive).with(encode "Apple = ")
47
- parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65]
48
- parse [0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
49
- end
50
-
51
- it "parses fragmented frames" do
52
- @web_socket.should_receive(:receive).with("Hello")
53
- parse [0x00, 0x48, 0x65, 0x6c]
54
- parse [0x6c, 0x6f, 0xff]
55
- end
56
- end
57
-
58
14
  it_should_behave_like "draft-75 parser"
59
15
  end
60
16
 
@@ -4,6 +4,7 @@ require 'thin'
4
4
  require 'rainbows'
5
5
  require File.expand_path('../../lib/faye/websocket', __FILE__)
6
6
  require File.expand_path('../../vendor/em-rspec/lib/em-rspec', __FILE__)
7
+ require File.expand_path('../faye/websocket/draft75_parser_examples', __FILE__)
7
8
 
8
9
  Thin::Logging.silent = true
9
10
  Unicorn::Configurator::DEFAULTS[:logger] = Logger.new(StringIO.new)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: faye-websocket
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-13 00:00:00.000000000 Z
12
+ date: 2012-02-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
16
- requirement: &18778460 !ruby/object:Gem::Requirement
16
+ requirement: &21030040 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.12.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *18778460
24
+ version_requirements: *21030040
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rack
27
- requirement: &18778060 !ruby/object:Gem::Requirement
27
+ requirement: &21055040 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *18778060
35
+ version_requirements: *21055040
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rainbows
38
- requirement: &18777520 !ruby/object:Gem::Requirement
38
+ requirement: &21054500 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.0.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *18777520
46
+ version_requirements: *21054500
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &18777020 !ruby/object:Gem::Requirement
49
+ requirement: &21054000 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 2.8.0
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *18777020
57
+ version_requirements: *21054000
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake-compiler
60
- requirement: &18776640 !ruby/object:Gem::Requirement
60
+ requirement: &21053620 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *18776640
68
+ version_requirements: *21053620
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: thin
71
- requirement: &18776100 !ruby/object:Gem::Requirement
71
+ requirement: &21053080 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,7 +76,7 @@ dependencies:
76
76
  version: 1.2.0
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *18776100
79
+ version_requirements: *21053080
80
80
  description:
81
81
  email: jcoglan@gmail.com
82
82
  executables: []
@@ -115,6 +115,7 @@ files:
115
115
  - examples/autobahn_client.rb
116
116
  - spec/faye/websocket/draft75_parser_spec.rb
117
117
  - spec/faye/websocket/client_spec.rb
118
+ - spec/faye/websocket/draft75_parser_examples.rb
118
119
  - spec/faye/websocket/draft76_parser_spec.rb
119
120
  - spec/faye/websocket/hybi_parser_spec.rb
120
121
  - spec/server.key