├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── Rakefile
├── VERSION
├── examples
└── echochat.rb
├── lib
├── sinatra-websocket.rb
└── sinatra-websocket
│ ├── error.rb
│ ├── ext
│ ├── sinatra
│ │ └── request.rb
│ └── thin
│ │ └── connection.rb
│ └── version.rb
├── sinatra-websocket.gemspec
└── spec
├── error_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | Gemfile
3 | Gemfile.lock
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.3.1
2 | - [#10](https://github.com/simulacre/sinatra-websocket/issues/10): Event machine not initialized error on close
3 | - [#12](https://github.com/simulacre/sinatra-websocket/issues/12): Don't use Thin 2.0.0
4 |
5 | ## 0.3.0
6 | - [#6](https://github.com/simulacre/sinatra-websocket/pull/6): check for the existence of async.orig_callback - [@crazed](https://github.com/crazed)
7 |
8 | ## 0.2.1
9 | - [#3](https://github.com/simulacre/sinatra-websocket/pull/3): Lock em-websocket dependency due to latest API changes. - [@pedrocarrico](https://github.com/pedrocarrico)
10 |
11 | ## 0.2.0
12 | - [#2](https://github.com/simulacre/sinatra-websocket/pull/2): Update gem spec to work with latest thin (1.5.0) - [@pedrocarrico](https://github.com/pedrocarrico)
13 |
14 | ## 0.1.2
15 | - [#1](https://github.com/simulacre/sinatra-websocket/pull/1): Sinatra::Request#websocket? will check if HTTP_CONNECTION and HTTP_UPGRADE headers exist before examining them - [@ttencate](https://github.com/ttencate)
16 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Caleb Crane
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 | Portion of this software are Copyright (c) Bernard Potocki
23 | Portion of this software are Copyright (c) 2010 Samuel Cochran
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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/imanel/websocket-rack). It provides helpers methods to detect if a request is a WebSocket request and defer to an [EM::WebSocket::Connection](https://github.com/igrigorik/em-websocket).
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("websocket closed")
32 | settings.sockets.delete(ws)
33 | end
34 | end
35 | end
36 | end
37 |
38 | __END__
39 | @@ index
40 |
41 |
42 | Simple Echo & Chat Server
43 |
46 |
47 |
48 |
49 |
73 |
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 and Samuel Cochran.
89 |
90 | See License.txt for more details.
91 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | version_file = File.expand_path __FILE__ + '/../VERSION'
4 | version = File.read(version_file).strip
5 |
6 | spec_file = File.expand_path __FILE__ + '/../skinny.gemspec'
7 | spec = Gem::Specification.load spec_file
8 |
9 | require 'rdoc/task'
10 | RDoc::Task.new :rdoc => "rdoc",
11 | :clobber_rdoc => "rdoc:clean",
12 | :rerdoc => "rdoc:force" do |rdoc|
13 | rdoc.title = "Skinny #{version}"
14 | rdoc.rdoc_dir = 'rdoc'
15 | rdoc.main = 'README.md'
16 | rdoc.rdoc_files.include 'lib/**/*.rb'
17 | end
18 |
19 | desc "Package as Gem"
20 | task "package:gem" do
21 | builder = Gem::Builder.new spec
22 | builder.build
23 | end
24 |
25 | task "package" => ["package:gem"]
26 |
27 | desc "Release Gem to RubyGems"
28 | task "release:gem" do
29 | %x[gem push skinny-#{version}.gem]
30 | end
31 |
32 | task "release" => ["package", "release:gem"]
33 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.2.0
2 |
--------------------------------------------------------------------------------
/examples/echochat.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # ruby examples/echochat.rb
3 |
4 | $: << File.expand_path('../../lib/', __FILE__)
5 | require 'sinatra'
6 | require 'sinatra-websocket'
7 |
8 | set :server, 'thin'
9 | set :sockets, []
10 |
11 | get '/' do
12 | if !request.websocket?
13 | erb :index
14 | else
15 | request.websocket do |ws|
16 | ws.onopen do
17 | ws.send("Hello World!")
18 | settings.sockets << ws
19 | end
20 | ws.onmessage do |msg|
21 | EM.next_tick { settings.sockets.each{|s| s.send(msg) } }
22 | end
23 | ws.onclose do
24 | warn("wetbsocket closed")
25 | settings.sockets.delete(ws)
26 | end
27 | end
28 | end
29 | end
30 |
31 | __END__
32 | @@ index
33 |
34 |
35 | Simple Echo & Chat Server
36 |
39 |
40 |
41 |
42 |
66 |
67 |
--------------------------------------------------------------------------------
/lib/sinatra-websocket.rb:
--------------------------------------------------------------------------------
1 | require 'thin'
2 | require 'em-websocket'
3 | require 'sinatra-websocket/error'
4 | require 'sinatra-websocket/ext/thin/connection'
5 | require 'sinatra-websocket/ext/sinatra/request'
6 |
7 | module SinatraWebsocket
8 | class Connection < ::EventMachine::WebSocket::Connection
9 | class << self
10 | def from_env(env, options = {})
11 | if env.include?('async.orig_callback')
12 | callback_key = 'async.orig_callback'
13 | elsif env.include?(Thin::Request::ASYNC_CALLBACK)
14 | callback_key = Thin::Request::ASYNC_CALLBACK
15 | else
16 | raise Error::ConfigurationError.new('Could not find an async callback in our environment!')
17 | end
18 | socket = env[callback_key].receiver
19 | request = request_from_env(env)
20 | connection = Connection.new(env, socket, :debug => options[:debug])
21 | yield(connection) if block_given?
22 | connection.dispatch(request) ? async_response : failure_response
23 | end
24 |
25 | #######
26 | # Taken from WebSocket Rack
27 | # https://github.com/imanel/websocket-rack
28 | #######
29 |
30 | # Parse Rack env to em-websocket-compatible format
31 | # this probably should be moved to Base in future
32 | def request_from_env(env)
33 | request = {}
34 | request['path'] = env['REQUEST_URI'].to_s
35 | request['method'] = env['REQUEST_METHOD']
36 | request['query'] = env['QUERY_STRING'].to_s
37 | request['Body'] = env['rack.input'].read
38 |
39 | env.each do |key, value|
40 | if key.match(/HTTP_(.+)/)
41 | request[$1.downcase.gsub('_','-')] ||= value
42 | end
43 | end
44 | request
45 | end
46 |
47 | # Standard async response
48 | def async_response
49 | [-1, {}, []]
50 | end
51 |
52 | # Standard 400 response
53 | def failure_response
54 | [ 400, {'Content-Type' => 'text/plain'}, [ 'Bad request' ] ]
55 | end
56 | end # class << self
57 |
58 |
59 | #########################
60 | ### EventMachine part ###
61 | #########################
62 |
63 | # Overwrite new from EventMachine
64 | # we need to skip standard procedure called
65 | # when socket is created - this is just a stub
66 | def self.new(*args)
67 | instance = allocate
68 | instance.__send__(:initialize, *args)
69 | instance
70 | end
71 |
72 | # Overwrite send_data from EventMachine
73 | # delegate send_data to rack server
74 | def send_data(*args)
75 | EM.next_tick do
76 | @socket.send_data(*args)
77 | end
78 | end
79 |
80 | # Overwrite close_connection from EventMachine
81 | # delegate close_connection to rack server
82 | def close_connection(*args)
83 | EM.next_tick do
84 | @socket.close_connection(*args)
85 | end
86 | end
87 |
88 | #########################
89 | ### EM-WebSocket part ###
90 | #########################
91 |
92 | # Overwrite initialize from em-websocket
93 | # set all standard options and disable
94 | # EM connection inactivity timeout
95 | def initialize(app, socket, options = {})
96 | @app = app
97 | @socket = socket
98 | @options = options
99 | @debug = options[:debug] || false
100 | @ssl = socket.backend.respond_to?(:ssl?) && socket.backend.ssl?
101 |
102 | socket.websocket = self
103 | socket.comm_inactivity_timeout = 0
104 |
105 | debug [:initialize]
106 | end
107 |
108 | def get_peername
109 | @socket.get_peername
110 | end
111 |
112 | # Overwrite dispath from em-websocket
113 | # we already have request headers parsed so
114 | # we can skip it and call build_with_request
115 | def dispatch(data)
116 | return false if data.nil?
117 | debug [:inbound_headers, data]
118 | @handler = EventMachine::WebSocket::HandlerFactory.build_with_request(self, data, data['Body'], @ssl, @debug)
119 | unless @handler
120 | # The whole header has not been received yet.
121 | return false
122 | end
123 | @handler.run
124 | return true
125 | end
126 | end
127 | end # module::SinatraWebSocket
128 |
--------------------------------------------------------------------------------
/lib/sinatra-websocket/error.rb:
--------------------------------------------------------------------------------
1 | module SinatraWebsocket
2 | module Error
3 | class StandardError < ::StandardError
4 | include Error
5 | end
6 | class ConfigurationError < StandardError; end
7 | class ConnectionError < StandardError; end
8 | end
9 | end # module::SinatraWebsocket
10 |
--------------------------------------------------------------------------------
/lib/sinatra-websocket/ext/sinatra/request.rb:
--------------------------------------------------------------------------------
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'] && env['HTTP_UPGRADE'] &&
9 | env['HTTP_CONNECTION'].split(',').map(&:strip).map(&:downcase).include?('upgrade') &&
10 | env['HTTP_UPGRADE'].downcase == 'websocket'
11 | end
12 |
13 | # Taken from skinny https://github.com/sj26/skinny
14 | def websocket(options={}, &blk)
15 | env['skinny.websocket'] ||= begin
16 | raise Error::ConnectionError.new("Not a WebSocket request") unless websocket?
17 | SinatraWebsocket::Connection.from_env(env, options, &blk)
18 | end
19 | end
20 | end
21 | end # module::Sinatra
22 | end # module::Ext
23 | end # module::SinatraWebsocket
24 | defined?(Sinatra) && Sinatra::Request.send(:include, SinatraWebsocket::Ext::Sinatra::Request)
25 |
--------------------------------------------------------------------------------
/lib/sinatra-websocket/ext/thin/connection.rb:
--------------------------------------------------------------------------------
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 | self.websocket? && self.websocket.unbind
48 | unbind_without_websocket
49 | end
50 |
51 | # Send flash policy file if requested
52 | def receive_data_with_flash_policy_file(data)
53 | # thin require data to be proper http request - in it's not
54 | # then @request.parse raises exception and data isn't parsed
55 | # by futher methods. Here we only check if it is flash
56 | # policy file request ("\000") and
57 | # if so then flash policy file is returned. if not then
58 | # rest of request is handled.
59 | if (data == "\000")
60 | file = ''
61 | # ignore errors - we will close this anyway
62 | send_data(file) rescue nil
63 | close_connection_after_writing
64 | else
65 | receive_data_without_flash_policy_file(data)
66 | end
67 | end
68 |
69 | end # module::Connection
70 | end # module::Thin
71 | end # module::Ext
72 | end # module::SinatraWebsocket
73 | defined?(Thin) && Thin::Connection.send(:include, SinatraWebsocket::Ext::Thin::Connection)
74 |
--------------------------------------------------------------------------------
/lib/sinatra-websocket/version.rb:
--------------------------------------------------------------------------------
1 | module SinatraWebsocket
2 | VERSION = '0.3.1'
3 | end
4 |
5 |
--------------------------------------------------------------------------------
/sinatra-websocket.gemspec:
--------------------------------------------------------------------------------
1 | require File.expand_path('../lib/sinatra-websocket/version', __FILE__)
2 |
3 | Gem::Specification.new do |s|
4 | s.name = 'sinatra-websocket'
5 | s.version = SinatraWebsocket::VERSION
6 | s.summary = "Simple, upgradable WebSockets for Sinatra."
7 | s.description = "Makes it easy to upgrade any request to a websocket connection in Sinatra"
8 | s.homepage = 'http://github.com/simulacre/sinatra-websocket'
9 | s.email = 'sinatra-websocket@simulacre.org'
10 | s.authors = ['Caleb Crane']
11 | s.files = Dir["lib/**/*.rb", "bin/*", "*.md"]
12 | s.require_paths = ["lib"]
13 |
14 | s.add_dependency 'eventmachine'
15 | s.add_dependency 'thin', '>= 1.3.1', '<2.0.0'
16 | s.add_dependency 'em-websocket', '~>0.3.6'
17 | end
18 |
--------------------------------------------------------------------------------
/spec/error_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../spec_helper", __FILE__)
2 |
3 | describe SinatraWebsocket::Error::StandardError do
4 | it "should be tagged with SinatraWebsocket::Error" do
5 | subject.should be_a SinatraWebsocket::Error
6 | end
7 | end
8 |
9 | describe SinatraWebsocket::Error::ConfigurationError do
10 | it "should be tagged with SinatraWebsocket::Error" do
11 | subject.should be_a SinatraWebsocket::Error
12 | end
13 | end
14 |
15 | describe SinatraWebsocket::Error::ConnectionError do
16 | it "should be tagged with SinatraWebsocket::Error" do
17 | subject.should be_a SinatraWebsocket::Error
18 | end
19 | end
20 |
21 | describe "Sinatra::Request" do
22 | context "the request is not a websocket request" do
23 | subject { Sinatra::Request.new({})}
24 | describe "#websocket?" do
25 | it { subject.websocket?.should be_nil }
26 | end
27 | describe "#websocket" do
28 | it do
29 | expect {
30 | subject.websocket
31 | }.to raise_error(SinatraWebsocket::Error::ConnectionError)
32 | end
33 | end
34 | end
35 | end
36 |
37 | describe "Sinatra::Websoket::Connection" do
38 | context "the request does not have an async callback" do
39 | describe ".from_env" do
40 | it "should raise SinatraWebsocket::Error::ConfigurationError" do
41 | expect {
42 | SinatraWebsocket::Connection.from_env({})
43 | }.to raise_error(SinatraWebsocket::Error::ConfigurationError)
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "rspec"
2 | require "sinatra"
3 | require "sinatra-websocket"
4 |
5 | module SinatraWebsocket
6 | module Test
7 |
8 | end
9 | end
10 |
--------------------------------------------------------------------------------