skinny 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+