_bushido-faye-websocket 0.4.4

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.
Files changed (40) hide show
  1. data/CHANGELOG.txt +56 -0
  2. data/README.rdoc +366 -0
  3. data/examples/app.rb +50 -0
  4. data/examples/autobahn_client.rb +44 -0
  5. data/examples/client.rb +30 -0
  6. data/examples/config.ru +17 -0
  7. data/examples/haproxy.conf +21 -0
  8. data/examples/server.rb +44 -0
  9. data/examples/sse.html +39 -0
  10. data/examples/ws.html +44 -0
  11. data/ext/faye_websocket_mask/FayeWebsocketMaskService.java +61 -0
  12. data/ext/faye_websocket_mask/extconf.rb +5 -0
  13. data/ext/faye_websocket_mask/faye_websocket_mask.c +33 -0
  14. data/lib/faye/adapters/goliath.rb +47 -0
  15. data/lib/faye/adapters/rainbows.rb +32 -0
  16. data/lib/faye/adapters/rainbows_client.rb +70 -0
  17. data/lib/faye/adapters/thin.rb +62 -0
  18. data/lib/faye/eventsource.rb +124 -0
  19. data/lib/faye/websocket.rb +216 -0
  20. data/lib/faye/websocket/adapter.rb +21 -0
  21. data/lib/faye/websocket/api.rb +96 -0
  22. data/lib/faye/websocket/api/event.rb +33 -0
  23. data/lib/faye/websocket/api/event_target.rb +34 -0
  24. data/lib/faye/websocket/client.rb +84 -0
  25. data/lib/faye/websocket/draft75_parser.rb +87 -0
  26. data/lib/faye/websocket/draft76_parser.rb +84 -0
  27. data/lib/faye/websocket/hybi_parser.rb +320 -0
  28. data/lib/faye/websocket/hybi_parser/handshake.rb +78 -0
  29. data/lib/faye/websocket/hybi_parser/stream_reader.rb +29 -0
  30. data/lib/faye/websocket/utf8_match.rb +8 -0
  31. data/spec/faye/websocket/client_spec.rb +179 -0
  32. data/spec/faye/websocket/draft75_parser_examples.rb +48 -0
  33. data/spec/faye/websocket/draft75_parser_spec.rb +27 -0
  34. data/spec/faye/websocket/draft76_parser_spec.rb +34 -0
  35. data/spec/faye/websocket/hybi_parser_spec.rb +156 -0
  36. data/spec/rainbows.conf +3 -0
  37. data/spec/server.crt +15 -0
  38. data/spec/server.key +15 -0
  39. data/spec/spec_helper.rb +68 -0
  40. metadata +158 -0
@@ -0,0 +1,62 @@
1
+ # WebSocket extensions for Thin
2
+ # Based on code from the Cramp project
3
+ # http://github.com/lifo/cramp
4
+
5
+ # Copyright (c) 2009-2011 Pratik Naik
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ class Thin::Connection
27
+ attr_accessor :socket_stream
28
+
29
+ alias :thin_process :process
30
+ alias :thin_receive_data :receive_data
31
+
32
+ def process
33
+ if @serving != :websocket and @request.websocket?
34
+ @serving = :websocket
35
+ end
36
+ if @request.async_connection?
37
+ @request.env['em.connection'] = self
38
+ @response.persistent!
39
+ @response.async = true
40
+ end
41
+ thin_process
42
+ end
43
+
44
+ def receive_data(data)
45
+ return thin_receive_data(data) unless @serving == :websocket
46
+ socket_stream.receive(data) if socket_stream
47
+ end
48
+ end
49
+
50
+ class Thin::Request
51
+ include Faye::WebSocket::Adapter
52
+ end
53
+
54
+ class Thin::Response
55
+ attr_accessor :async
56
+ alias :thin_head :head
57
+
58
+ def head
59
+ async ? '' : thin_head
60
+ end
61
+ end
62
+
@@ -0,0 +1,124 @@
1
+ require File.expand_path('../websocket', __FILE__) unless defined?(Faye::WebSocket)
2
+
3
+ module Faye
4
+ class EventSource
5
+ DEFAULT_RETRY = 5
6
+
7
+ include WebSocket::API
8
+ attr_reader :env, :url, :ready_state
9
+
10
+ def self.eventsource?(env)
11
+ accept = (env['HTTP_ACCEPT'] || '').split(/\s*,\s*/)
12
+ accept.include?('text/event-stream')
13
+ end
14
+
15
+ def self.determine_url(env)
16
+ secure = if env.has_key?('HTTP_X_FORWARDED_PROTO')
17
+ env['HTTP_X_FORWARDED_PROTO'] == 'https'
18
+ else
19
+ env['HTTP_ORIGIN'] =~ /^https:/i
20
+ end
21
+
22
+ scheme = secure ? 'https:' : 'http:'
23
+ "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
24
+ end
25
+
26
+ def initialize(env, options = {})
27
+ @env = env
28
+ @ping = options[:ping]
29
+ @retry = (options[:retry] || DEFAULT_RETRY).to_f
30
+ @url = EventSource.determine_url(env)
31
+ @stream = Stream.new(self)
32
+
33
+ @ready_state = CONNECTING
34
+ @send_buffer = []
35
+ EventMachine.next_tick { open }
36
+
37
+ callback = @env['async.callback']
38
+ callback.call([101, {}, @stream])
39
+
40
+ @stream.write("HTTP/1.1 200 OK\r\n" +
41
+ "Content-Type: text/event-stream\r\n" +
42
+ "Cache-Control: no-cache, no-store\r\n" +
43
+ "Connection: close\r\n" +
44
+ "\r\n\r\n" +
45
+ "retry: #{ (@retry * 1000).floor }\r\n\r\n")
46
+
47
+ @ready_state = OPEN
48
+
49
+ if @ping
50
+ @ping_timer = EventMachine.add_periodic_timer(@ping) { ping }
51
+ end
52
+ end
53
+
54
+ def last_event_id
55
+ @env['HTTP_LAST_EVENT_ID'] || ''
56
+ end
57
+
58
+ def rack_response
59
+ [ -1, {}, [] ]
60
+ end
61
+
62
+ def send(message, options = {})
63
+ return false unless @ready_state == OPEN
64
+
65
+ message = WebSocket.encode(message.to_s).
66
+ gsub(/(\r\n|\r|\n)/, '\1data: ')
67
+
68
+ frame = ""
69
+ frame << "event: #{options[:event]}\r\n" if options[:event]
70
+ frame << "id: #{options[:id]}\r\n" if options[:id]
71
+ frame << "data: #{message}\r\n\r\n"
72
+
73
+ @stream.write(frame)
74
+ true
75
+ end
76
+
77
+ def ping(message = nil)
78
+ @stream.write(":\r\n\r\n")
79
+ true
80
+ end
81
+
82
+ def close
83
+ return if [CLOSING, CLOSED].include?(@ready_state)
84
+ @ready_state = CLOSED
85
+ EventMachine.cancel_timer(@ping_timer)
86
+ @stream.close_connection_after_writing
87
+ event = WebSocket::API::Event.new('close')
88
+ event.init_event('close', false, false)
89
+ dispatch_event(event)
90
+ end
91
+ end
92
+
93
+ class EventSource::Stream
94
+ include EventMachine::Deferrable
95
+
96
+ extend Forwardable
97
+ def_delegators :@connection, :close_connection, :close_connection_after_writing
98
+
99
+ def initialize(event_source)
100
+ @event_source = event_source
101
+ @connection = event_source.env['em.connection']
102
+ @stream_send = event_source.env['stream.send']
103
+
104
+ @connection.socket_stream = self if @connection.respond_to?(:socket_stream)
105
+ end
106
+
107
+ def each(&callback)
108
+ @stream_send ||= callback
109
+ end
110
+
111
+ def fail
112
+ @event_source.close
113
+ end
114
+
115
+ def receive(data)
116
+ end
117
+
118
+ def write(data)
119
+ return unless @stream_send
120
+ @stream_send.call(data) rescue nil
121
+ end
122
+ end
123
+ end
124
+
@@ -0,0 +1,216 @@
1
+ # API and protocol references:
2
+ #
3
+ # * http://dev.w3.org/html5/websockets/
4
+ # * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-eventtarget
5
+ # * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-event
6
+ # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
7
+ # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
8
+ # * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
9
+
10
+ require 'base64'
11
+ require 'digest/md5'
12
+ require 'digest/sha1'
13
+ require 'forwardable'
14
+ require 'net/http'
15
+ require 'stringio'
16
+ require 'uri'
17
+ require 'eventmachine'
18
+
19
+ module Faye
20
+ autoload :EventSource, File.expand_path('../eventsource', __FILE__)
21
+
22
+ class WebSocket
23
+ root = File.expand_path('../websocket', __FILE__)
24
+ require root + '/../../faye_websocket_mask'
25
+
26
+ def self.jruby?
27
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
28
+ end
29
+
30
+ def self.rbx?
31
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
32
+ end
33
+
34
+ if jruby?
35
+ require 'jruby'
36
+ com.jcoglan.faye.FayeWebsocketMaskService.new.basicLoad(JRuby.runtime)
37
+ end
38
+
39
+ unless WebSocketMask.respond_to?(:mask)
40
+ def WebSocketMask.mask(payload, mask)
41
+ @instance ||= new
42
+ @instance.mask(payload, mask)
43
+ end
44
+ end
45
+
46
+ unless String.instance_methods.include?(:force_encoding)
47
+ require root + '/utf8_match'
48
+ end
49
+
50
+ autoload :Adapter, root + '/adapter'
51
+ autoload :API, root + '/api'
52
+ autoload :Client, root + '/client'
53
+ autoload :Draft75Parser, root + '/draft75_parser'
54
+ autoload :Draft76Parser, root + '/draft76_parser'
55
+ autoload :HybiParser, root + '/hybi_parser'
56
+
57
+ ADAPTERS = {
58
+ 'thin' => :Thin,
59
+ 'rainbows' => :Rainbows,
60
+ 'goliath' => :Goliath
61
+ }
62
+
63
+ def self.load_adapter(backend)
64
+ const = Kernel.const_get(ADAPTERS[backend]) rescue nil
65
+ require(backend) unless const
66
+ require File.expand_path("../adapters/#{backend}", __FILE__)
67
+ end
68
+
69
+ def self.utf8_string(string)
70
+ string = string.pack('C*') if Array === string
71
+ string.respond_to?(:force_encoding) ?
72
+ string.force_encoding('UTF-8') :
73
+ string
74
+ end
75
+
76
+ def self.encode(string, validate_encoding = false)
77
+ if Array === string
78
+ string = utf8_string(string)
79
+ return nil if validate_encoding and !valid_utf8?(string)
80
+ end
81
+ utf8_string(string)
82
+ end
83
+
84
+ def self.valid_utf8?(byte_array)
85
+ string = utf8_string(byte_array)
86
+ if defined?(UTF8_MATCH)
87
+ UTF8_MATCH =~ string ? true : false
88
+ else
89
+ string.valid_encoding?
90
+ end
91
+ end
92
+
93
+ def self.websocket?(env)
94
+ env['REQUEST_METHOD'] == 'GET' and
95
+ env['HTTP_CONNECTION'] and
96
+ env['HTTP_CONNECTION'].split(/\s*,\s*/).include?('Upgrade') and
97
+ ['WebSocket', 'websocket'].include?(env['HTTP_UPGRADE'])
98
+ end
99
+
100
+ def self.parser(env)
101
+ if env['HTTP_SEC_WEBSOCKET_VERSION']
102
+ HybiParser
103
+ elsif env['HTTP_SEC_WEBSOCKET_KEY1']
104
+ Draft76Parser
105
+ else
106
+ Draft75Parser
107
+ end
108
+ end
109
+
110
+ def self.determine_url(env)
111
+ secure = if env.has_key?('HTTP_X_FORWARDED_PROTO')
112
+ env['HTTP_X_FORWARDED_PROTO'] == 'https'
113
+ else
114
+ env['HTTP_ORIGIN'] =~ /^https:/i
115
+ end
116
+
117
+ scheme = secure ? 'wss:' : 'ws:'
118
+ "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
119
+ end
120
+
121
+ extend Forwardable
122
+ def_delegators :@parser, :version
123
+
124
+ attr_reader :env
125
+ include API
126
+
127
+ def initialize(env, supported_protos = nil, options = {})
128
+ @env = env
129
+ @stream = Stream.new(self)
130
+ @ping = options[:ping]
131
+ @ping_id = 0
132
+
133
+ @url = WebSocket.determine_url(@env)
134
+ @ready_state = CONNECTING
135
+ @buffered_amount = 0
136
+
137
+ @parser = WebSocket.parser(@env).new(self, :protocols => supported_protos)
138
+
139
+ @send_buffer = []
140
+ EventMachine.next_tick { open }
141
+
142
+ @callback = @env['async.callback']
143
+ @callback.call([101, {}, @stream])
144
+ @stream.write(@parser.handshake_response)
145
+
146
+ @ready_state = OPEN if @parser.open?
147
+
148
+ if @ping
149
+ @ping_timer = EventMachine.add_periodic_timer(@ping) do
150
+ @ping_id += 1
151
+ ping(@ping_id.to_s)
152
+ end
153
+ end
154
+ end
155
+
156
+ def ping(message = '', &callback)
157
+ return false unless @parser.respond_to?(:ping)
158
+ @parser.ping(message, &callback)
159
+ end
160
+
161
+ def protocol
162
+ @parser.protocol || ''
163
+ end
164
+
165
+ def rack_response
166
+ [ -1, {}, [] ]
167
+ end
168
+
169
+ private
170
+
171
+ def parse(data)
172
+ response = @parser.parse(data)
173
+ return unless response
174
+ @stream.write(response)
175
+ open
176
+ end
177
+ end
178
+
179
+ class WebSocket::Stream
180
+ include EventMachine::Deferrable
181
+
182
+ extend Forwardable
183
+ def_delegators :@connection, :close_connection, :close_connection_after_writing
184
+
185
+ def initialize(web_socket)
186
+ @web_socket = web_socket
187
+ @connection = web_socket.env['em.connection']
188
+ @stream_send = web_socket.env['stream.send']
189
+
190
+ @connection.socket_stream = self if @connection.respond_to?(:socket_stream)
191
+ end
192
+
193
+ def each(&callback)
194
+ @stream_send ||= callback
195
+ end
196
+
197
+ def fail
198
+ @web_socket.close(1006, '', false)
199
+ end
200
+
201
+ def receive(data)
202
+ @web_socket.__send__(:parse, data)
203
+ end
204
+
205
+ def write(data)
206
+ return unless @stream_send
207
+ @stream_send.call(data) rescue nil
208
+ end
209
+ end
210
+ end
211
+
212
+ Faye::WebSocket::ADAPTERS.each do |name, const|
213
+ klass = Kernel.const_get(const) rescue nil
214
+ Faye::WebSocket.load_adapter(name) if klass
215
+ end
216
+
@@ -0,0 +1,21 @@
1
+ module Faye
2
+ class WebSocket
3
+
4
+ module Adapter
5
+ def websocket?
6
+ e = defined?(@env) ? @env : env
7
+ WebSocket.websocket?(e)
8
+ end
9
+
10
+ def eventsource?
11
+ e = defined?(@env) ? @env : env
12
+ EventSource.eventsource?(e)
13
+ end
14
+
15
+ def async_connection?
16
+ websocket? or eventsource?
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,96 @@
1
+ module Faye
2
+ class WebSocket
3
+
4
+ module API
5
+ module ReadyStates
6
+ CONNECTING = 0
7
+ OPEN = 1
8
+ CLOSING = 2
9
+ CLOSED = 3
10
+ end
11
+
12
+ class IllegalStateError < StandardError
13
+ end
14
+
15
+ require File.expand_path('../api/event_target', __FILE__)
16
+ require File.expand_path('../api/event', __FILE__)
17
+ include EventTarget
18
+ include ReadyStates
19
+
20
+ attr_reader :url, :ready_state, :buffered_amount
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
+
40
+ def receive(data)
41
+ return false unless @ready_state == OPEN
42
+ event = Event.new('message')
43
+ event.init_event('message', false, false)
44
+ event.data = data
45
+ dispatch_event(event)
46
+ end
47
+
48
+ def send(data, type = nil, error_type = nil)
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
+
60
+ data = data.to_s unless Array === data
61
+
62
+ data = WebSocket.encode(data) if String === data
63
+ frame = @parser.frame(data, type, error_type)
64
+ @stream.write(frame) if frame
65
+ end
66
+
67
+ def close(code = nil, reason = nil, ack = true)
68
+ return if [CLOSING, CLOSED].include?(@ready_state)
69
+
70
+ @ready_state = CLOSING
71
+
72
+ close = lambda do
73
+ @ready_state = CLOSED
74
+ EventMachine.cancel_timer(@ping_timer) if @ping_timer
75
+ @stream.close_connection_after_writing
76
+ event = Event.new('close', :code => code || 1000, :reason => reason || '')
77
+ event.init_event('close', false, false)
78
+ dispatch_event(event)
79
+ end
80
+
81
+ if ack
82
+ if @parser.respond_to?(:close)
83
+ @parser.close(code, reason, &close)
84
+ else
85
+ close.call
86
+ end
87
+ else
88
+ @parser.close(code, reason) if @parser.respond_to?(:close)
89
+ close.call
90
+ end
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+