skinny 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.
Files changed (7) hide show
  1. data/.gitignore +1 -0
  2. data/LICENSE +20 -0
  3. data/README.md +54 -0
  4. data/Rakefile +33 -0
  5. data/VERSION +1 -0
  6. data/lib/skinny.rb +237 -0
  7. metadata +100 -0
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Samuel Cochran
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.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Skinny
2
+
3
+ Simple, upgradable Thin WebSockets.
4
+
5
+ I wanted to be able to upgrade a plain old Rack request to a proper
6
+ WebSocket. The easiest way seemed to use the oh-so-nice-and-clean
7
+ [Thin][thin] with a new pair of skinnies.
8
+
9
+ More details coming soon.
10
+
11
+ ## Examples
12
+
13
+ More comprehensive examples will be coming soon. Here's a really
14
+ simple, not-yet-optimised example I'm using at the moment:
15
+
16
+ module MailCatcher
17
+ class Web < Sinatra::Base
18
+ get '/messages' do
19
+ if request.websocket?
20
+ request.websocket! :protocol => "MailCatcher 0.2 Message Push",
21
+ :on_start => proc do |websocket|
22
+ subscription = MailCatcher::Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
23
+ websocket.on_close do |websocket|
24
+ MailCatcher::Events::MessageAdded.unsubscribe subscription
25
+ end
26
+ end
27
+ else
28
+ MailCatcher::Mail.messages.to_json
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ This syntax will probably get cleaned up. I would like to build a
35
+ nice Sinatra handler with DSL with unbound handlers so Sinatra
36
+ requests can be recycled.
37
+
38
+ ## TODO
39
+
40
+ * Nicer
41
+ * Documentation
42
+ * Tests
43
+ * Make more generic for alternate server implementations?
44
+
45
+ ## Copyright
46
+
47
+ Copyright (c) 2010 Samuel Cochran. See LICENSE for details.
48
+
49
+ ## Wear Them
50
+
51
+ [Do you?][jeans]
52
+
53
+ [thin]: http://code.macournoyer.com/thin/
54
+ [jeans]: http://www.shaunoakes.com/images/skinny-jeans-no.jpg
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "skinny"
8
+ gem.summary = %Q{Thin WebSockets}
9
+ gem.description = <<-EOD
10
+ Simple, upgradable WebSockets for Thin.
11
+ EOD
12
+ gem.email = "sj26@sj26.com"
13
+ gem.homepage = "http://github.com/sj26/skinny"
14
+ gem.authors = ["Samuel Cochran"]
15
+
16
+ gem.add_dependency 'eventmachine'
17
+ gem.add_dependency 'thin'
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/rdoctask'
25
+ Rake::RDocTask.new do |rdoc|
26
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
27
+
28
+ rdoc.rdoc_dir = 'rdoc'
29
+ rdoc.title = "skinny #{version}"
30
+ rdoc.rdoc_files.include('README*')
31
+ rdoc.rdoc_files.include('lib/*.rb')
32
+ rdoc.rdoc_files.include('lib/**/*.rb')
33
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/skinny.rb ADDED
@@ -0,0 +1,237 @@
1
+ require 'eventmachine'
2
+ require 'digest/md5'
3
+ require 'thin'
4
+
5
+ module Skinny
6
+ module Callbacks
7
+ def self.included base
8
+ base.class_eval do
9
+ extend ClassMethods
10
+ include InstanceMethods
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def define_callback *names
16
+ names.each do |name|
17
+ define_method name do |&block|
18
+ add_callback name, &block
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ def add_callback name, &block
26
+ @callbacks ||= {}
27
+ @callbacks[name] ||= []
28
+ @callbacks[name] << block
29
+ end
30
+
31
+ def callback name, *args, &block
32
+ return [] if @callbacks.nil? || @callbacks[name].nil?
33
+ @callbacks[name].collect { |callback| callback.call *args, &block }
34
+ end
35
+ end
36
+ end
37
+
38
+ class WebSocketError < RuntimeError; end
39
+ class WebSocketProtocolError < WebSocketError; end
40
+
41
+ class Websocket < EventMachine::Connection
42
+ include Callbacks
43
+
44
+ define_callback :on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close
45
+
46
+ # 4mb is almost too generous, imho.
47
+ MAX_BUFFER_LENGTH = 2 ** 32
48
+
49
+ def self.from_env env, options={}
50
+ # Steal the connection
51
+ thin_connection = env[Thin::Request::ASYNC_CALLBACK].receiver
52
+ # We have all the events now, muahaha
53
+ EM.attach(thin_connection.detach, self, env, options)
54
+ end
55
+
56
+ def initialize env, options={}
57
+ @env = env.dup
58
+ @buffer = ''
59
+
60
+ self.protocol = options.delete :protocol if options.has_key? :protocol
61
+ [:on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close].each do |name|
62
+ send name, &options.delete(name) if options.has_key?(name)
63
+ end
64
+ raise ArgumentError, "Unknown options: #{options.inspect}" unless options.empty?
65
+
66
+ EM.next_tick { callback :on_open, self }
67
+ end
68
+
69
+ # Return an async response -- stops Thin doing anything with connection.
70
+ def response
71
+ Thin::Connection::AsyncResponse
72
+ end
73
+
74
+ # Arrayify self into a response tuple
75
+ alias :to_a :response
76
+
77
+ def start!
78
+ # Steal any remaining data from rack.input
79
+ @buffer = @env[Thin::Request::RACK_INPUT].read + @buffer
80
+
81
+ # Remove references to Thin connection objects, freeing memory
82
+ @env.delete Thin::Request::RACK_INPUT
83
+ @env.delete Thin::Request::ASYNC_CALLBACK
84
+ @env.delete Thin::Request::ASYNC_CLOSE
85
+
86
+ EM.next_tick { callback :on_start, self }
87
+
88
+ # Queue up the actual handshake
89
+ EM.next_tick method :handshake!
90
+
91
+ # Return self so we can be used as a response
92
+ self
93
+ rescue
94
+ error! $!
95
+ end
96
+
97
+ def protocol
98
+ @env['HTTP_SEC_WEBSOCKET_PROTOCOL']
99
+ end
100
+
101
+ def protocol= value
102
+ @env['HTTP_SEC_WEBSOCKET_PROTOCOL'] = value
103
+ end
104
+
105
+ [1, 2].each do |i|
106
+ define_method "key#{i}" do
107
+ key = @env["HTTP_SEC_WEBSOCKET_KEY#{i}"]
108
+ key.scan(/[0-9]/).join.to_i / key.count(' ')
109
+ end
110
+ end
111
+
112
+ def key3
113
+ @key3 ||= @buffer.slice!(0...8)
114
+ end
115
+
116
+ def challenge?
117
+ @env.has_key? 'HTTP_SEC_WEBSOCKET_KEY1'
118
+ end
119
+
120
+ def challenge
121
+ [key1, key2].pack("N*") + key3
122
+ end
123
+
124
+ def challenge_response
125
+ Digest::MD5.digest(challenge)
126
+ end
127
+
128
+ def handshake
129
+ "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
130
+ "Connection: Upgrade\r\n" +
131
+ "Upgrade: WebSocket\r\n" +
132
+ "Sec-WebSocket-Location: ws#{@env['rack.url_scheme'] == 'https' ? 's' : ''}://#{@env['HTTP_HOST']}#{@env['REQUEST_PATH']}\r\n" +
133
+ "Sec-WebSocket-Origin: #{@env['HTTP_ORIGIN']}\r\n" +
134
+ ("Sec-WebSocket-Protocol: #{@env['HTTP_SEC_WEBSOCKET_PROTOCOL']}\r\n" if @env['HTTP_SEC_WEBSOCKET_PROTOCOL']) +
135
+ "\r\n" +
136
+ "#{challenge_response}"
137
+ end
138
+
139
+ def handshake!
140
+ [key1, key2].each { |key| raise WebSocketProtocolError, "Invalid key: #{key}" if key >= 2**32 }
141
+ # XXX: Should we wait for 8 bytes?
142
+ raise WebSocketProtocolError, "Invalid challenge: #{key3}" if key3.length < 8
143
+
144
+ send_data handshake
145
+ @handshook = true
146
+
147
+ EM.next_tick { callback :on_handshake, self }
148
+ rescue
149
+ error! $!
150
+ end
151
+
152
+ def receive_data data
153
+ @buffer += data
154
+
155
+ EM.next_tick { process_frame } if @handshook
156
+ rescue
157
+ error! $!
158
+ end
159
+
160
+ def process_frame
161
+ if @buffer.length >= 1
162
+ if @buffer[0] == "\x00"
163
+ if ending = @buffer.index("\xff")
164
+ frame = @buffer.slice! 0..ending
165
+ message = frame[1..-2]
166
+
167
+ EM.next_tick { receive_message message }
168
+ elsif @buffer.length > MAX_BUFFER_LENGTH
169
+ error! "Maximum buffer length (#{MAX_BUFFER_LENGTH}) exceeded: #{@buffer.length}"
170
+ end
171
+ elsif @buffer[0] == "\xff"
172
+ if @buffer.length > 1
173
+ if @buffer[1] == "\x00"
174
+ @buffer.slice! 0..1
175
+
176
+ EM.next_tick { finish! }
177
+ else
178
+ error! "Incorrect finish frame length: #{@buffer[1].inspect}"
179
+ end
180
+ end
181
+ else
182
+ error! "Unknown frame type: #{@buffer[0].inspect}"
183
+ end
184
+ end
185
+ end
186
+
187
+ def receive_message message
188
+ EM.next_tick { callback :on_message, self, message }
189
+ end
190
+
191
+ def frame_message message
192
+ "\x00#{message}\xff"
193
+ end
194
+
195
+ def send_message message
196
+ send_data frame_message(message)
197
+ end
198
+
199
+ def error! message=nil
200
+ EM.next_tick { callback :on_error, self }
201
+ EM.next_tick { finish! } unless @finished
202
+ # XXX: Log or something
203
+ puts "Websocket Error: #{$!}"
204
+ end
205
+
206
+ def finish!
207
+ send_data "\xff\x00"
208
+ close_connection_after_writing
209
+ @finished = true
210
+
211
+ EM.next_tick { callback :on_finish, self }
212
+ rescue
213
+ error! $!
214
+ end
215
+
216
+ def unbind
217
+ EM.next_tick { callback :on_close, self }
218
+ end
219
+ end
220
+
221
+ module RequestHelpers
222
+ def websocket?
223
+ @env['HTTP_CONNECTION'] == 'Upgrade' && @env['HTTP_UPGRADE'] == 'WebSocket'
224
+ end
225
+
226
+ def websocket(options={})
227
+ @env['skinny.websocket'] ||= begin
228
+ raise RuntimerError, "Not a WebSocket request" unless websocket?
229
+ Websocket.from_env(@env, options)
230
+ end
231
+ end
232
+
233
+ def websocket!(options={})
234
+ websocket(options).start!
235
+ end
236
+ end
237
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skinny
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Samuel Cochran
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-28 00:00:00 +08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: eventmachine
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: thin
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: " Simple, upgradable WebSockets for Thin.\n"
50
+ email: sj26@sj26.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - LICENSE
57
+ - README.md
58
+ files:
59
+ - .gitignore
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - VERSION
64
+ - lib/skinny.rb
65
+ has_rdoc: true
66
+ homepage: http://github.com/sj26/skinny
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options:
71
+ - --charset=UTF-8
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project:
95
+ rubygems_version: 1.3.7
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Thin WebSockets
99
+ test_files: []
100
+