websocket-rack 0.1.4 → 0.2.1

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.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