sinatra-websocket 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|