sinatra-websocket 0.1.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.
- data/README.md +90 -0
- data/lib/sinatra-websocket.rb +115 -0
- data/lib/sinatra-websocket/ext/sinatra/request.rb +23 -0
- data/lib/sinatra-websocket/ext/thin/connection.rb +76 -0
- data/lib/sinatra-websocket/version.rb +4 -0
- metadata +97 -0
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# SinatraWebsocket
|
2
|
+
|
3
|
+
Makes it easy to upgrade any request to a websocket connection.
|
4
|
+
|
5
|
+
SinatraWebsocket is a fork of [Skinny](https://github.com/sj26/skinny) merged with [Rack WebSocket](https://github.com/igrigorik/em-websocket). It provides helpers methods to detect if an request is a WebSocket request and defer to an EM::WebSocket::Connection.
|
6
|
+
|
7
|
+
|
8
|
+
## Put this in your pipe ...
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
|
12
|
+
require 'sinatra'
|
13
|
+
require 'sinatra-websocket'
|
14
|
+
|
15
|
+
set :server, 'thin'
|
16
|
+
set :sockets, []
|
17
|
+
|
18
|
+
get '/' do
|
19
|
+
if !request.websocket?
|
20
|
+
erb :index
|
21
|
+
else
|
22
|
+
request.websocket do |ws|
|
23
|
+
ws.onopen do
|
24
|
+
ws.send("Hello World!")
|
25
|
+
settings.sockets << ws
|
26
|
+
end
|
27
|
+
ws.onmessage do |msg|
|
28
|
+
EM.next_tick { settings.sockets.each{|s| s.send(msg) } }
|
29
|
+
end
|
30
|
+
ws.onclose do
|
31
|
+
warn("wetbsocket closed")
|
32
|
+
settings.sockets.delete(ws)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
__END__
|
39
|
+
@@ index
|
40
|
+
<html>
|
41
|
+
<body>
|
42
|
+
<h1>Simple Echo & Chat Server</h1>
|
43
|
+
<form id="form">
|
44
|
+
<input type="text" id="input" value="send a message"></input>
|
45
|
+
</form>
|
46
|
+
<div id="msgs"></div>
|
47
|
+
</body>
|
48
|
+
|
49
|
+
<script type="text/javascript">
|
50
|
+
window.onload = function(){
|
51
|
+
(function(){
|
52
|
+
var show = function(el){
|
53
|
+
return function(msg){ el.innerHTML = msg + '<br />' + el.innerHTML; }
|
54
|
+
}(document.getElementById('msgs'));
|
55
|
+
|
56
|
+
var ws = new WebSocket('ws://' + window.location.host + window.location.pathname);
|
57
|
+
ws.onopen = function() { show('websocket opened'); };
|
58
|
+
ws.onclose = function() { show('websocket closed'); }
|
59
|
+
ws.onmessage = function(m) { show('websocket message: ' + m.data); };
|
60
|
+
|
61
|
+
var sender = function(f){
|
62
|
+
var input = document.getElementById('input');
|
63
|
+
input.onclick = function(){ input.value = "" };
|
64
|
+
f.onsubmit = function(){
|
65
|
+
ws.send(input.value);
|
66
|
+
input.value = "send a message";
|
67
|
+
return false;
|
68
|
+
}
|
69
|
+
}(document.getElementById('form'));
|
70
|
+
})();
|
71
|
+
}
|
72
|
+
</script>
|
73
|
+
</html>
|
74
|
+
|
75
|
+
```
|
76
|
+
|
77
|
+
## And Smoke It
|
78
|
+
|
79
|
+
```
|
80
|
+
ruby echo.rb
|
81
|
+
```
|
82
|
+
|
83
|
+
|
84
|
+
## Copyright
|
85
|
+
|
86
|
+
Copyright (c) 2012 Caleb Crane.
|
87
|
+
|
88
|
+
Portions of this software are Copyright (c) Bernard Potocki <bernard.potocki@imanel.org> and Samuel Cochran.
|
89
|
+
|
90
|
+
See License.txt for more details.
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'thin'
|
2
|
+
require 'em-websocket'
|
3
|
+
require 'sinatra-websocket/ext/thin/connection'
|
4
|
+
require 'sinatra-websocket/ext/sinatra/request'
|
5
|
+
|
6
|
+
module SinatraWebsocket
|
7
|
+
class Connection < ::EventMachine::WebSocket::Connection
|
8
|
+
class << self
|
9
|
+
def from_env(env, options = {})
|
10
|
+
socket = env[Thin::Request::ASYNC_CALLBACK].receiver
|
11
|
+
request = request_from_env(env)
|
12
|
+
connection = Connection.new(env, socket, :debug => options[:debug])
|
13
|
+
yield(connection) if block_given?
|
14
|
+
connection.dispatch(request) ? async_response : failure_response
|
15
|
+
end
|
16
|
+
|
17
|
+
#######
|
18
|
+
# Taken from WebSocket Rack
|
19
|
+
# https://github.com/imanel/websocket-rack
|
20
|
+
#######
|
21
|
+
|
22
|
+
# Parse Rack env to em-websocket-compatible format
|
23
|
+
# this probably should be moved to Base in future
|
24
|
+
def request_from_env(env)
|
25
|
+
request = {}
|
26
|
+
request['path'] = env['REQUEST_URI'].to_s
|
27
|
+
request['method'] = env['REQUEST_METHOD']
|
28
|
+
request['query'] = env['QUERY_STRING'].to_s
|
29
|
+
request['Body'] = env['rack.input'].read
|
30
|
+
|
31
|
+
env.each do |key, value|
|
32
|
+
if key.match(/HTTP_(.+)/)
|
33
|
+
request[$1.downcase.gsub('_','-')] ||= value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
request
|
37
|
+
end
|
38
|
+
|
39
|
+
# Standard async response
|
40
|
+
def async_response
|
41
|
+
[-1, {}, []]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Standard 400 response
|
45
|
+
def failure_response
|
46
|
+
[ 400, {'Content-Type' => 'text/plain'}, [ 'Bad request' ] ]
|
47
|
+
end
|
48
|
+
end # class << self
|
49
|
+
|
50
|
+
|
51
|
+
#########################
|
52
|
+
### EventMachine part ###
|
53
|
+
#########################
|
54
|
+
|
55
|
+
# Overwrite new from EventMachine
|
56
|
+
# we need to skip standard procedure called
|
57
|
+
# when socket is created - this is just a stub
|
58
|
+
def self.new(*args)
|
59
|
+
instance = allocate
|
60
|
+
instance.__send__(:initialize, *args)
|
61
|
+
instance
|
62
|
+
end
|
63
|
+
|
64
|
+
# Overwrite send_data from EventMachine
|
65
|
+
# delegate send_data to rack server
|
66
|
+
def send_data(*args)
|
67
|
+
EM.next_tick do
|
68
|
+
@socket.send_data(*args)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Overwrite close_connection from EventMachine
|
73
|
+
# delegate close_connection to rack server
|
74
|
+
def close_connection(*args)
|
75
|
+
EM.next_tick do
|
76
|
+
@socket.close_connection(*args)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#########################
|
81
|
+
### EM-WebSocket part ###
|
82
|
+
#########################
|
83
|
+
|
84
|
+
# Overwrite initialize from em-websocket
|
85
|
+
# set all standard options and disable
|
86
|
+
# EM connection inactivity timeout
|
87
|
+
def initialize(app, socket, options = {})
|
88
|
+
@app = app
|
89
|
+
@socket = socket
|
90
|
+
@options = options
|
91
|
+
@debug = options[:debug] || false
|
92
|
+
@ssl = socket.backend.respond_to?(:ssl?) && socket.backend.ssl?
|
93
|
+
|
94
|
+
socket.websocket = self
|
95
|
+
socket.comm_inactivity_timeout = 0
|
96
|
+
|
97
|
+
debug [:initialize]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Overwrite dispath from em-websocket
|
101
|
+
# we already have request headers parsed so
|
102
|
+
# we can skip it and call build_with_request
|
103
|
+
def dispatch(data)
|
104
|
+
return false if data.nil?
|
105
|
+
debug [:inbound_headers, data]
|
106
|
+
@handler = EventMachine::WebSocket::HandlerFactory.build_with_request(self, data, data['Body'], @ssl, @debug)
|
107
|
+
unless @handler
|
108
|
+
# The whole header has not been received yet.
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
@handler.run
|
112
|
+
return true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end # module::SinatraWebSocket
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SinatraWebsocket
|
2
|
+
module Ext
|
3
|
+
module Sinatra
|
4
|
+
module Request
|
5
|
+
|
6
|
+
# Taken from skinny https://github.com/sj26/skinny and updated to support Firefox
|
7
|
+
def websocket?
|
8
|
+
env['HTTP_CONNECTION'].split(',').map(&:strip).map(&:downcase).include?('upgrade') &&
|
9
|
+
env['HTTP_UPGRADE'].downcase == 'websocket'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Taken from skinny https://github.com/sj26/skinny
|
13
|
+
def websocket(options={}, &blk)
|
14
|
+
env['skinny.websocket'] ||= begin
|
15
|
+
raise RuntimeError, "Not a WebSocket request" unless websocket?
|
16
|
+
SinatraWebsocket::Connection.from_env(env, options, &blk)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # module::Sinatra
|
21
|
+
end # module::Ext
|
22
|
+
end # module::SinatraWebsocket
|
23
|
+
defined?(Sinatra) && Sinatra::Request.send(:include, SinatraWebsocket::Ext::Sinatra::Request)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module SinatraWebsocket
|
2
|
+
module Ext
|
3
|
+
module Thin
|
4
|
+
module Connection
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
alias :receive_data_without_websocket :receive_data
|
8
|
+
alias :receive_data :receive_data_with_websocket
|
9
|
+
|
10
|
+
alias :unbind_without_websocket :unbind
|
11
|
+
alias :unbind :unbind_with_websocket
|
12
|
+
|
13
|
+
alias :receive_data_without_flash_policy_file :receive_data
|
14
|
+
alias :receive_data :receive_data_with_flash_policy_file
|
15
|
+
|
16
|
+
alias :pre_process_without_websocket :pre_process
|
17
|
+
alias :pre_process :pre_process_with_websocket
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :websocket
|
22
|
+
|
23
|
+
# Set 'async.connection' Rack env
|
24
|
+
def pre_process_with_websocket
|
25
|
+
@request.env['async.connection'] = self
|
26
|
+
pre_process_without_websocket
|
27
|
+
end
|
28
|
+
|
29
|
+
# Is this connection WebSocket?
|
30
|
+
def websocket?
|
31
|
+
!self.websocket.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Skip default receive_data if this is
|
35
|
+
# WebSocket connection
|
36
|
+
def receive_data_with_websocket(data)
|
37
|
+
if self.websocket?
|
38
|
+
self.websocket.receive_data(data)
|
39
|
+
else
|
40
|
+
receive_data_without_websocket(data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Skip standard unbind it this is
|
45
|
+
# WebSocket connection
|
46
|
+
def unbind_with_websocket
|
47
|
+
if self.websocket?
|
48
|
+
self.websocket.unbind
|
49
|
+
else
|
50
|
+
unbind_without_websocket
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Send flash policy file if requested
|
55
|
+
def receive_data_with_flash_policy_file(data)
|
56
|
+
# thin require data to be proper http request - in it's not
|
57
|
+
# then @request.parse raises exception and data isn't parsed
|
58
|
+
# by futher methods. Here we only check if it is flash
|
59
|
+
# policy file request ("<policy-file-request/>\000") and
|
60
|
+
# if so then flash policy file is returned. if not then
|
61
|
+
# rest of request is handled.
|
62
|
+
if (data == "<policy-file-request/>\000")
|
63
|
+
file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
|
64
|
+
# ignore errors - we will close this anyway
|
65
|
+
send_data(file) rescue nil
|
66
|
+
close_connection_after_writing
|
67
|
+
else
|
68
|
+
receive_data_without_flash_policy_file(data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end # module::Connection
|
73
|
+
end # module::Thin
|
74
|
+
end # module::Ext
|
75
|
+
end # module::SinatraWebsocket
|
76
|
+
defined?(Thin) && Thin::Connection.send(:include, SinatraWebsocket::Ext::Thin::Connection)
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-websocket
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Caleb Crane
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: thin
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.3.1
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.3.1
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: em-websocket
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.3.6
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.3.6
|
62
|
+
description: Makes it easy to upgrade any request to a websocket connection in Sinatra
|
63
|
+
email: sinatra-websocket@simulacre.org
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- lib/sinatra-websocket/ext/sinatra/request.rb
|
69
|
+
- lib/sinatra-websocket/ext/thin/connection.rb
|
70
|
+
- lib/sinatra-websocket/version.rb
|
71
|
+
- lib/sinatra-websocket.rb
|
72
|
+
- README.md
|
73
|
+
homepage: http://github.com/simulacre/sinatra-websocket
|
74
|
+
licenses: []
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.8.21
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Simple, upgradable WebSockets for Sinatra.
|
97
|
+
test_files: []
|