websocket-rack 0.1.4 → 0.2.1

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.md +11 -0
  2. data/README.md +42 -24
  3. data/Rakefile +1 -1
  4. data/example/example.ru +5 -5
  5. data/lib/rack/websocket/application.rb +31 -60
  6. data/lib/rack/websocket/extensions/common.rb +61 -0
  7. data/lib/rack/websocket/extensions/thin/connection.rb +2 -50
  8. data/lib/rack/websocket/extensions/thin.rb +3 -3
  9. data/lib/rack/websocket/extensions.rb +14 -0
  10. data/lib/rack/websocket/handler/base.rb +41 -0
  11. data/lib/rack/websocket/handler/stub.rb +14 -0
  12. data/lib/rack/websocket/handler/thin/connection.rb +89 -0
  13. data/lib/rack/websocket/handler/thin/handler_factory.rb +56 -0
  14. data/lib/rack/websocket/handler/thin.rb +61 -0
  15. data/lib/rack/websocket/handler.rb +15 -38
  16. data/lib/rack/websocket/version.rb +5 -0
  17. data/lib/rack/websocket.rb +5 -31
  18. data/spec/spec_helper.rb +18 -0
  19. data/spec/support/all_drafts.rb +43 -0
  20. data/spec/support/all_handlers.rb +31 -0
  21. data/spec/support/requests.rb +100 -0
  22. data/spec/thin_spec.rb +46 -0
  23. data/websocket-rack.gemspec +4 -4
  24. metadata +41 -47
  25. data/lib/rack/websocket/connection.rb +0 -112
  26. data/lib/rack/websocket/debugger.rb +0 -17
  27. data/lib/rack/websocket/framing03.rb +0 -178
  28. data/lib/rack/websocket/framing76.rb +0 -115
  29. data/lib/rack/websocket/handler03.rb +0 -14
  30. data/lib/rack/websocket/handler75.rb +0 -8
  31. data/lib/rack/websocket/handler76.rb +0 -11
  32. data/lib/rack/websocket/handler_factory.rb +0 -61
  33. data/lib/rack/websocket/handshake75.rb +0 -21
  34. data/lib/rack/websocket/handshake76.rb +0 -71
  35. data/spec/helper.rb +0 -44
  36. data/spec/integration/draft03_spec.rb +0 -252
  37. data/spec/integration/draft76_spec.rb +0 -212
  38. data/spec/unit/framing_spec.rb +0 -108
  39. data/spec/unit/handler_spec.rb +0 -136
  40. data/spec/websocket_spec.rb +0 -210
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 / 2011-04-01
4
+
5
+ - bugfix: env passed to callbacks should be valid now
6
+
7
+ ## 0.2.0 / 2011-04-01
8
+
9
+ - prepare for supporting server other that thin
10
+ - small changes of API
11
+ - change handling backend options
12
+ - depend on em-websocket instead of copying source
13
+
3
14
  ## 0.1.4 / 2011-03-13
4
15
 
5
16
  - performance improvements thanks to rbtrace
data/README.md CHANGED
@@ -12,7 +12,7 @@ Create sample rack config file, and inside build app basing on Rack::WebSocket::
12
12
  end
13
13
 
14
14
  map '/' do
15
- run MyApp
15
+ run MyApp.new
16
16
  end
17
17
 
18
18
  After that just run Rack config from Rack server:
@@ -25,50 +25,50 @@ Done.
25
25
 
26
26
  Rack::WebSocket::Application make following methods available:
27
27
 
28
- ### on_open
28
+ ### on_open(env)
29
29
 
30
- Called after client is connected.
30
+ Called after client is connected. Rack env of client is passed as attribute.
31
31
 
32
32
  Example:
33
33
 
34
34
  class MyApp < Rack::WebSocket::Application
35
- def on_open
35
+ def on_open(env)
36
36
  puts "Clien connected"
37
37
  end
38
38
  end
39
39
 
40
- ### on_close
40
+ ### on_close(env)
41
41
 
42
- Called after client is disconnected
42
+ Called after client is disconnected. Rack env of client is passed as attribute.
43
43
 
44
44
  Example:
45
45
 
46
46
  class MyApp < Rack::WebSocket::Application
47
- def on_close
47
+ def on_close(env)
48
48
  puts "Clien disconnected"
49
49
  end
50
50
  end
51
51
 
52
- ### on_message(msg)
52
+ ### on_message(env, msg)
53
53
 
54
- Called after server receive message
54
+ Called after server receive message. Rack env of client is passed as attribute.
55
55
 
56
56
  Example:
57
57
 
58
58
  class MyApp < Rack::WebSocket::Application
59
- def on_message(msg)
59
+ def on_message(env, msg)
60
60
  puts "Received message: " + msg
61
61
  end
62
62
  end
63
63
 
64
- ### on_error(error)
64
+ ### on_error(env, error)
65
65
 
66
66
  Called after server catch error. Variable passed is instance of Ruby Exception class.
67
67
 
68
68
  Example:
69
69
 
70
70
  class MyApp < Rack::WebSocket::Application
71
- def on_error(error)
71
+ def on_error(env, error)
72
72
  puts "Error occured: " + error.message
73
73
  end
74
74
  end
@@ -80,34 +80,50 @@ Sends data do client.
80
80
  Example:
81
81
 
82
82
  class MyApp < Rack::WebSocket::Application
83
- def on_open
83
+ def on_open(env)
84
84
  send_data "Hello to you!"
85
85
  end
86
86
  end
87
87
 
88
+ ### close_websocket
89
+
90
+ Closes connection.
91
+
92
+ Example:
93
+
94
+ class MyApp < Rack::WebSocket::Application
95
+ def on_open(env)
96
+ close_websocket if env['REQUEST_PATH'] != '/websocket'
97
+ end
98
+ end
99
+
88
100
  ## Available variables:
89
101
 
90
- ### @env
102
+ ### @options
91
103
 
92
- Rack env - contain all data sent by client when connectind.
104
+ Options passed to app on initialize.
93
105
 
94
- ### @conn
106
+ Example:
95
107
 
96
- Connection - thin wrapper between client and EventMachine::Connection
108
+ # In config.ru
109
+ map '/' do
110
+ run MyApp.new :some => :variable
111
+ end
112
+
113
+ # In application instance
114
+ @options # => { :some => :variable }
97
115
 
98
116
  ## FAQ
99
117
 
100
118
  ### Which Rack servers are supported?
101
119
 
102
- Currently only Thin. I plan to support also Rainbows! in future, but not yet.
120
+ Currently we are supporting following servers:
103
121
 
104
- ### Why (using Thin) user is disconnected after 30 seconds?
105
-
106
- This is bug in EventMachine < 1.0.0. Please consider updating to newer version or use thin-websocket wrapper around thin binary.
122
+ - Thin
107
123
 
108
124
  ### How to enable debugging?
109
125
 
110
- Just use :websocket\_debug => true option when initializing your app.
126
+ Just use :backend => { :debug => true } option when initializing your app.
111
127
 
112
128
  ### How to enable wss/SSL support?
113
129
 
@@ -117,10 +133,12 @@ Thin v1.2.8 have --ssl option - just use that! :)
117
133
 
118
134
  Check [Thin](http://code.macournoyer.com/thin/) config - any option supported by Thin(like demonizing, SSL etc.) is supported by WebSocket-Rack.
119
135
 
136
+ ### Why (using Thin) user is disconnected after 30 seconds?
137
+
138
+ This is bug in EventMachine < 1.0.0. Please consider updating to newer version or use thin-websocket wrapper around thin binary.
139
+
120
140
  ## About
121
141
 
122
142
  Author: Bernard Potocki <<bernard.potocki@imanel.org>>
123
143
 
124
- Most source taken from [em-websocket](http://github.com/igrigorik/em-websocket)
125
-
126
144
  Released under MIT license.
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ Bundler::GemHelper.install_tasks
4
4
  require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new do |t|
7
- t.rspec_opts = ["-c", "-f progress", "-r ./spec/helper.rb"]
7
+ t.rspec_opts = ["-c", "-f progress"]
8
8
  t.pattern = 'spec/**/*_spec.rb'
9
9
  end
10
10
 
data/example/example.ru CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'lib/rack/websocket'
2
2
 
3
3
  class MyApp < Rack::WebSocket::Application
4
- def on_open
4
+ def on_open(env)
5
5
  puts "client connected"
6
6
  EM.add_timer(5) do
7
7
  send_data "This message should show-up 5 secs later"
@@ -12,12 +12,12 @@ class MyApp < Rack::WebSocket::Application
12
12
  end
13
13
  end
14
14
 
15
- def on_message(msg)
15
+ def on_message(env, msg)
16
16
  puts "message received: " + msg
17
- send_data "Hello #{msg}"
17
+ send_data "Message: #{msg}"
18
18
  end
19
19
 
20
- def on_close
20
+ def on_close(env)
21
21
  puts "client disconnected"
22
22
  end
23
23
  end
@@ -29,5 +29,5 @@ map '/' do
29
29
  end
30
30
 
31
31
  map '/websocket' do
32
- run MyApp.new # :websocket_debug => true
32
+ run MyApp.new # :backend => { :debug => true }
33
33
  end
@@ -2,77 +2,48 @@ module Rack
2
2
  module WebSocket
3
3
  class Application
4
4
 
5
+ DEFAULT_OPTIONS = {}
6
+
5
7
  class << self
6
- def new(*args)
7
- if args.last == {:real_run => true}
8
- args.pop
9
- super(*args)
10
- else
11
- proc do |env|
12
- self.new(*(args << {:real_run => true})).call(env)
13
- end
14
- end
15
- end
8
+ attr_accessor :websocket_handler
16
9
  end
17
-
18
- DEFAULT_OPTIONS = {}
19
- attr_accessor :options
20
-
21
- def on_open; end # Fired when a client is connected.
22
- def on_message(msg); end # Fired when a message from a client is received.
23
- def on_close; end # Fired when a client is disconnected.
24
- def on_error(error); end # Fired when error occurs.
25
-
26
- def initialize(*args)
27
- app, options = args[0], args[1]
28
- app, options = nil, app if app.is_a?(Hash)
29
- @options = DEFAULT_OPTIONS.merge(options || {})
30
- @app = app
10
+
11
+ def on_open(env); end
12
+ def on_message(env, msg); end
13
+ def on_close(env); end
14
+ def on_error(env, error); end
15
+
16
+ def initialize(options = {})
17
+ @options = DEFAULT_OPTIONS.merge(options)
31
18
  end
32
-
19
+
33
20
  def call(env)
34
- if(env['HTTP_CONNECTION'].to_s.downcase == 'upgrade' && env['HTTP_UPGRADE'].to_s.downcase == 'websocket')
35
- @env = env
36
- socket = env['async.connection']
37
- @conn = Connection.new(self, socket, :debug => !!@options[:websocket_debug])
38
- @conn.dispatch(env) ? async_response : failure_response
39
- elsif @app
40
- @app.call(env)
41
- else
42
- not_found_response
43
- end
21
+ detect_handler(env)
22
+ dup._call(env)
44
23
  end
45
-
46
- def close_websocket
47
- if @conn
48
- @conn.close_websocket
49
- else
50
- raise WebSocketError, "WebSocket not opened"
51
- end
24
+
25
+ def _call(env)
26
+ websocket_handler.call(env)
52
27
  end
53
-
54
- def send_data(data)
55
- if @conn
56
- @conn.send data
28
+
29
+ def method_missing(sym, *args, &block)
30
+ if websocket_handler && websocket_handler.respond_to?(sym)
31
+ websocket_handler.send sym, *args, &block
57
32
  else
58
- raise WebSocketError, "WebSocket not opened"
33
+ super
59
34
  end
60
35
  end
61
-
62
- protected
63
-
64
- def async_response
65
- [-1, {}, []]
66
- end
67
-
68
- def failure_response
69
- [ 400, { "Content-Type" => "text/plain" }, [ 'invalid data' ] ]
36
+
37
+ private
38
+
39
+ def detect_handler(env)
40
+ self.class.websocket_handler ||= Handler.detect(env)
70
41
  end
71
-
72
- def not_found_response
73
- [ 404, { "Content-Type" => "text/plain" }, [ 'not found' ] ]
42
+
43
+ def websocket_handler
44
+ @websocket_handler ||= self.class.websocket_handler.new(self, @options || {})
74
45
  end
75
-
46
+
76
47
  end
77
48
  end
78
49
  end
@@ -0,0 +1,61 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Extensions
4
+ module Common
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ alias :receive_data_without_websocket :receive_data
9
+ alias :receive_data :receive_data_with_websocket
10
+
11
+ alias :unbind_without_websocket :unbind
12
+ alias :unbind :unbind_with_websocket
13
+
14
+ alias :receive_data_without_flash_policy_file :receive_data
15
+ alias :receive_data :receive_data_with_flash_policy_file
16
+ end
17
+ end
18
+
19
+ attr_accessor :websocket
20
+
21
+ def websocket?
22
+ !self.websocket.nil?
23
+ end
24
+
25
+ def receive_data_with_websocket(data)
26
+ if self.websocket?
27
+ self.websocket.receive_data(data)
28
+ else
29
+ receive_data_without_websocket(data)
30
+ end
31
+ end
32
+
33
+ def unbind_with_websocket
34
+ if self.websocket?
35
+ self.websocket.unbind
36
+ else
37
+ unbind_without_websocket
38
+ end
39
+ end
40
+
41
+ def receive_data_with_flash_policy_file(data)
42
+ # thin require data to be proper http request - in it's not
43
+ # then @request.parse raises exception and data isn't parsed
44
+ # by futher methods. Here we only check if it is flash
45
+ # policy file request ("<policy-file-request/>\000") and
46
+ # if so then flash policy file is returned. if not then
47
+ # rest of request is handled.
48
+ if (data == "<policy-file-request/>\000")
49
+ file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
50
+ # ignore errors - we will close this anyway
51
+ send_data(file) rescue nil
52
+ close_connection_after_writing
53
+ else
54
+ receive_data_without_flash_policy_file(data)
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,66 +4,18 @@ module Rack
4
4
  module Thin
5
5
  module Connection
6
6
 
7
- def self.included(thin_conn)
8
- thin_conn.class_eval do
7
+ def self.included(base)
8
+ base.class_eval do
9
9
  alias :pre_process_without_websocket :pre_process
10
10
  alias :pre_process :pre_process_with_websocket
11
-
12
- alias :receive_data_without_websocket :receive_data
13
- alias :receive_data :receive_data_with_websocket
14
-
15
- alias :unbind_without_websocket :unbind
16
- alias :unbind :unbind_with_websocket
17
-
18
- alias :receive_data_without_flash_policy_file :receive_data
19
- alias :receive_data :receive_data_with_flash_policy_file
20
11
  end
21
12
  end
22
13
 
23
- attr_accessor :websocket
24
-
25
- def websocket?
26
- !self.websocket.nil?
27
- end
28
-
29
14
  def pre_process_with_websocket
30
15
  @request.env['async.connection'] = self
31
16
  pre_process_without_websocket
32
17
  end
33
18
 
34
- def receive_data_with_websocket(data)
35
- if self.websocket?
36
- self.websocket.receive_data(data)
37
- else
38
- receive_data_without_websocket(data)
39
- end
40
- end
41
-
42
- def unbind_with_websocket
43
- if self.websocket?
44
- self.websocket.unbind
45
- else
46
- unbind_without_websocket
47
- end
48
- end
49
-
50
- def receive_data_with_flash_policy_file(data)
51
- # thin require data to be proper http request - in it's not
52
- # then @request.parse raises exception and data isn't parsed
53
- # by futher methods. Here we only check if it is flash
54
- # policy file request ("<policy-file-request/>\000") and
55
- # if so then flash policy file is returned. if not then
56
- # rest of request is handled.
57
- if (data == "<policy-file-request/>\000")
58
- file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
59
- # ignore errors - we will close this anyway
60
- send_data(file) rescue nil
61
- close_connection_after_writing
62
- else
63
- receive_data_without_flash_policy_file(data)
64
- end
65
- end
66
-
67
19
  end
68
20
  end
69
21
  end
@@ -5,9 +5,9 @@ module Rack
5
5
 
6
6
  autoload :Connection, "#{::File.dirname(__FILE__)}/thin/connection"
7
7
 
8
- def self.included(thin)
9
- thin_connection = thin.const_get(:Connection)
10
- thin_connection.send(:include, Thin.const_get(:Connection))
8
+ def self.apply!
9
+ ::Thin::Connection.send(:include, ::Rack::WebSocket::Extensions::Common)
10
+ ::Thin::Connection.send(:include, ::Rack::WebSocket::Extensions::Thin::Connection)
11
11
  end
12
12
 
13
13
  end
@@ -0,0 +1,14 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Extensions
4
+
5
+ autoload :Common, "#{ROOT_PATH}/websocket/extensions/common"
6
+ autoload :Thin, "#{ROOT_PATH}/websocket/extensions/thin"
7
+
8
+ def self.apply!
9
+ Thin.apply! if defined?(::Thin)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Base
5
+
6
+ def on_open; @parent.on_open(@env); end # Fired when a client is connected.
7
+ def on_message(msg); @parent.on_message(@env, msg); end # Fired when a message from a client is received.
8
+ def on_close; @parent.on_close(@env); end # Fired when a client is disconnected.
9
+ def on_error(error); @parent.on_error(@env, error); end # Fired when error occurs.
10
+
11
+ def initialize(parent, options = {})
12
+ @parent = parent
13
+ @options = options[:backend] || {}
14
+ end
15
+
16
+ def call(env)
17
+ raise 'Not implemented'
18
+ end
19
+
20
+ def send_data(data)
21
+ raise 'Not implemented'
22
+ end
23
+
24
+ def close_websocket
25
+ raise 'Not implemented'
26
+ end
27
+
28
+ protected
29
+
30
+ def async_response
31
+ [-1, {}, []]
32
+ end
33
+
34
+ def failure_response
35
+ [ 400, {'Content-Type' => 'text/plain'}, [ 'Bad request' ] ]
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Stub < Base
5
+
6
+ def call(env)
7
+ raise 'Unknown handler!'
8
+ close_websocket
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,89 @@
1
+ require 'addressable/uri'
2
+
3
+ module Rack
4
+ module WebSocket
5
+ module Handler
6
+ class Thin
7
+ class Connection < ::EventMachine::WebSocket::Connection
8
+
9
+ # Overwrite new from EventMachine
10
+ def self.new(*args)
11
+ instance = allocate
12
+ instance.__send__(:initialize, *args)
13
+ instance
14
+ end
15
+
16
+ def trigger_on_message(msg)
17
+ @app.on_message(msg)
18
+ end
19
+ def trigger_on_open
20
+ @app.on_open
21
+ end
22
+ def trigger_on_close
23
+ @app.on_close
24
+ end
25
+ def trigger_on_error(error)
26
+ @app.on_error(error)
27
+ end
28
+
29
+ def initialize(app, socket, options = {})
30
+ @app = app
31
+ @socket = socket
32
+ @options = options
33
+ @debug = options[:debug] || false
34
+ @ssl = socket.backend.respond_to?(:ssl?) && socket.backend.ssl?
35
+
36
+ socket.websocket = self
37
+ socket.comm_inactivity_timeout = 0
38
+
39
+ if socket.comm_inactivity_timeout != 0
40
+ puts "WARNING: You are using old EventMachine version. " +
41
+ "Please consider updating to EM version >= 1.0.0 " +
42
+ "or running Thin using thin-websocket."
43
+ end
44
+
45
+ debug [:initialize]
46
+ end
47
+
48
+ def dispatch(request)
49
+ return false if request.nil?
50
+ debug [:inbound_headers, request]
51
+ @handler = HandlerFactory.build_with_request(self, request, request['body'], @ssl, @debug)
52
+ unless @handler
53
+ # The whole header has not been received yet.
54
+ return false
55
+ end
56
+ @handler.run
57
+ return true
58
+ rescue => e
59
+ debug [:error, e]
60
+ process_bad_request(e)
61
+ return false
62
+ end
63
+
64
+ def process_bad_request(reason)
65
+ trigger_on_error(reason)
66
+ send_data "HTTP/1.1 400 Bad request\r\n\r\n"
67
+ close_connection_after_writing
68
+ end
69
+
70
+ def close_with_error(message)
71
+ trigger_on_error(message)
72
+ close_connection_after_writing
73
+ end
74
+
75
+ # Overwrite send_data from EventMachine
76
+ def send_data(*args)
77
+ @socket.send_data(*args)
78
+ end
79
+
80
+ # Overwrite close_connection from EventMachine
81
+ def close_connection(*args)
82
+ @socket.close_connection(*args)
83
+ end
84
+
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,56 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Thin
5
+ class HandlerFactory < ::EventMachine::WebSocket::HandlerFactory
6
+
7
+ # Bottom half of em-websocket HandlerFactory
8
+ # Taken from http://github.com/dj2/em-websocket
9
+ # This method is also used in experimental branch of Goliath
10
+ def self.build_with_request(connection, request, remains, secure = false, debug = false)
11
+ version = request['Sec-WebSocket-Key1'] ? 76 : 75
12
+ case version
13
+ when 75
14
+ if !remains.empty?
15
+ raise ::EventMachine::WebSocket::HandshakeError, "Extra bytes after header"
16
+ end
17
+ when 76
18
+ if remains.length < 8
19
+ # The whole third-key has not been received yet.
20
+ return nil
21
+ elsif remains.length > 8
22
+ raise ::EventMachine::WebSocket::HandshakeError, "Extra bytes after third key"
23
+ end
24
+ request['Third-Key'] = remains
25
+ else
26
+ raise ::EventMachine::WebSocket::WebSocketError, "Must not happen"
27
+ end
28
+
29
+ unless request['Connection'] == 'Upgrade' and request['Upgrade'] == 'WebSocket'
30
+ raise ::EventMachine::WebSocket::HandshakeError, "Connection and Upgrade headers required"
31
+ end
32
+
33
+ # transform headers
34
+ protocol = (secure ? "wss" : "ws")
35
+ request['Host'] = Addressable::URI.parse("#{protocol}://"+request['Host'])
36
+
37
+ if version = request['Sec-WebSocket-Draft']
38
+ if version == '1' || version == '2' || version == '3'
39
+ # We'll use handler03 - I believe they're all compatible
40
+ ::EventMachine::WebSocket::Handler03.new(connection, request, debug)
41
+ else
42
+ # According to spec should abort the connection
43
+ raise ::EventMachine::WebSocket::WebSocketError, "Unknown draft version: #{version}"
44
+ end
45
+ elsif request['Sec-WebSocket-Key1']
46
+ ::EventMachine::WebSocket::Handler76.new(connection, request, debug)
47
+ else
48
+ ::EventMachine::WebSocket::Handler75.new(connection, request, debug)
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end