_bushido-faye-websocket 0.4.4

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