faye-websocket 0.3.0 → 0.4.0

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.

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