tuttinator-skinny 0.2.4

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa026018fe67eabe05dea368eeb0a17e935e0673
4
+ data.tar.gz: ebadcfbbd7e7cc8ccbfe9495371b7de42d6d1f92
5
+ SHA512:
6
+ metadata.gz: 36008e090c2518879e98f4de9df07238e118b2e412d8ea1e6ba0ab22d822518a64bbf417c50ab59bb61cdea49f1e5932bb18db0c19a937bbdc9d347f21a71a23
7
+ data.tar.gz: e61f6a2002b5f5706073e9bcdf30f14c4e2c422ed53579fe358ea4e51f8db22310f58372bb0e1b2c8c5b9f1d245379e19dac65195a3c9705c4c3a489034159d2
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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,62 @@
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
+ class Sinatra::Request
17
+ include Skinny::Helpers
18
+ end
19
+
20
+ module MailCatcher
21
+ class Web < Sinatra::Base
22
+ get '/messages' do
23
+ if request.websocket?
24
+ request.websocket! :protocol => "MailCatcher 0.2 Message Push",
25
+ :on_start => proc do |websocket|
26
+ subscription = MailCatcher::Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
27
+ websocket.on_close do |websocket|
28
+ MailCatcher::Events::MessageAdded.unsubscribe subscription
29
+ end
30
+ end
31
+ else
32
+ MailCatcher::Mail.messages.to_json
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ This syntax will probably get cleaned up. I would like to build a
39
+ nice Sinatra handler with DSL with unbound handlers so Sinatra
40
+ requests can be recycled.
41
+
42
+ ## TODO
43
+
44
+ * Nicer
45
+ * Documentation
46
+ * Tests
47
+ * Make more generic for alternate server implementations?
48
+
49
+ ## Thanks
50
+
51
+ The latest WebSocket draft support is adapted from https://github.com/gimite/web-socket-ruby -- thank you!
52
+
53
+ ## Copyright
54
+
55
+ Copyright (c) 2010 Samuel Cochran. See LICENSE for details.
56
+
57
+ ## Wear Them
58
+
59
+ [Do you?][jeans]
60
+
61
+ [thin]: http://code.macournoyer.com/thin/
62
+ [jeans]: http://www.shaunoakes.com/images/skinny-jeans-no.jpg
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/lib/skinny.rb ADDED
@@ -0,0 +1,451 @@
1
+ require 'skinny/version'
2
+ require 'base64'
3
+ require 'eventmachine'
4
+ require 'digest/md5'
5
+ require 'thin'
6
+
7
+ module Skinny
8
+ module Callbacks
9
+ def self.included base
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def define_callback *names
18
+ names.each do |name|
19
+ define_method name do |&block|
20
+ add_callback name, &block
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+ def add_callback name, &block
28
+ @callbacks ||= {}
29
+ @callbacks[name] ||= []
30
+ @callbacks[name] << block
31
+ end
32
+
33
+ def callback name, *args, &block
34
+ return [] if @callbacks.nil? || @callbacks[name].nil?
35
+ @callbacks[name].collect { |callback| callback.call *args, &block }
36
+ end
37
+ end
38
+ end
39
+
40
+ class WebSocketError < RuntimeError; end
41
+ class WebSocketProtocolError < WebSocketError; end
42
+
43
+ # We need to be really careful not to throw an exception too high
44
+ # or we'll kill the server.
45
+ class Websocket < EventMachine::Connection
46
+ include Callbacks
47
+ include Thin::Logging
48
+
49
+ define_callback :on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close
50
+
51
+ # 4mb is almost too generous, imho.
52
+ MAX_BUFFER_LENGTH = 2 ** 32
53
+
54
+ GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
55
+
56
+ OPCODE_CONTINUATION = 0x00
57
+ OPCODE_TEXT = 0x01
58
+ OPCODE_BINARY = 0x02
59
+ OPCODE_CLOSE = 0x08
60
+ OPCODE_PING = 0x09
61
+ OPCODE_PONG = 0x0a
62
+
63
+ # Create a new WebSocket from a Thin::Request environment
64
+ def self.from_env env, options={}
65
+ # Pull the connection out of the env
66
+ thin_connection = env[Thin::Request::ASYNC_CALLBACK].receiver
67
+ # Steal the IO
68
+ fd = thin_connection.detach
69
+ # EventMachine 1.0.0 needs this to be closable
70
+ io = IO.for_fd(fd) unless fd.respond_to? :close
71
+ # We have all the events now, muahaha
72
+ EM.attach(io, self, env, options)
73
+ end
74
+
75
+ def initialize env, options={}
76
+ @env = env.dup
77
+ @buffer = ''
78
+
79
+ @protocol = options.delete :protocol if options.has_key? :protocol
80
+ [:on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close].each do |name|
81
+ send name, &options.delete(name) if options.has_key?(name)
82
+ end
83
+ raise ArgumentError, "Unknown options: #{options.inspect}" unless options.empty?
84
+ end
85
+
86
+ # Connection is now open
87
+ def post_init
88
+ EM.next_tick { callback :on_open, self rescue error! "Error in open callback" }
89
+ @state = :open
90
+ rescue
91
+ error! "Error opening connection"
92
+ end
93
+
94
+ # Return an async response -- stops Thin doing anything with connection.
95
+ def response
96
+ Thin::Connection::AsyncResponse
97
+ end
98
+
99
+ # Arrayify self into a response tuple
100
+ alias :to_a :response
101
+
102
+ # Start the websocket connection
103
+ def start!
104
+ # Steal any remaining data from rack.input
105
+ @buffer = @env[Thin::Request::RACK_INPUT].read + @buffer
106
+
107
+ # Remove references to Thin connection objects, freeing memory
108
+ @env.delete Thin::Request::RACK_INPUT
109
+ @env.delete Thin::Request::ASYNC_CALLBACK
110
+ @env.delete Thin::Request::ASYNC_CLOSE
111
+
112
+ # Figure out which version we're using
113
+ @version = @env['HTTP_SEC_WEBSOCKET_VERSION']
114
+ @version ||= "hixie-76" if @env.has_key?('HTTP_SEC_WEBSOCKET_KEY1') and @env.has_key?('HTTP_SEC_WEBSOCKET_KEY2')
115
+ @version ||= "hixie-75"
116
+
117
+ # Pull out the details we care about
118
+ @origin ||= @env['HTTP_SEC_WEBSOCKET_ORIGIN'] || @env['HTTP_ORIGIN']
119
+ @location ||= "ws#{secure? ? 's' : ''}://#{@env['HTTP_HOST']}#{@env['REQUEST_PATH']}"
120
+ @protocol ||= @env['HTTP_SEC_WEBSOCKET_PROTOCOL'] || @env['HTTP_WEBSOCKET_PROTOCOL']
121
+
122
+ EM.next_tick { callback :on_start, self rescue error! "Error in start callback" }
123
+
124
+ # Queue up the actual handshake
125
+ EM.next_tick method :handshake!
126
+
127
+ @state = :started
128
+
129
+ # Return self so we can be used as a response
130
+ self
131
+ rescue
132
+ error! "Error starting connection"
133
+ end
134
+
135
+ attr_reader :env, :version, :origin, :location, :protocol
136
+
137
+ def hixie_75?
138
+ @version == "hixie-75"
139
+ end
140
+
141
+ def hixie_76?
142
+ @version == "hixie-76"
143
+ end
144
+
145
+ def secure?
146
+ @env['HTTPS'] == 'on' or
147
+ # XXX: This could be faked... do we care?
148
+ @env['HTTP_X_FORWARDED_PROTO'] == 'https' or
149
+ @env['rack.url_scheme'] == 'https'
150
+ end
151
+
152
+ def key
153
+ @env['HTTP_SEC_WEBSOCKET_KEY']
154
+ end
155
+
156
+ [1, 2].each do |i|
157
+ define_method :"key#{i}" do
158
+ key = env["HTTP_SEC_WEBSOCKET_KEY#{i}"]
159
+ key.scan(/[0-9]/).join.to_i / key.count(' ')
160
+ end
161
+ end
162
+
163
+ def key3
164
+ @key3 ||= @buffer.slice!(0...8)
165
+ end
166
+
167
+ def challenge?
168
+ env.has_key? 'HTTP_SEC_WEBSOCKET_KEY1'
169
+ end
170
+
171
+ def challenge
172
+ if hixie_75?
173
+ nil
174
+ elsif hixie_76?
175
+ [key1, key2].pack("N*") + key3
176
+ else
177
+ key + GUID
178
+ end
179
+ end
180
+
181
+ def challenge_response
182
+ if hixie_75?
183
+ nil
184
+ elsif hixie_76?
185
+ Digest::MD5.digest(challenge)
186
+ else
187
+ Base64.encode64(Digest::SHA1.digest(challenge)).strip
188
+ end
189
+ end
190
+
191
+ # Generate the handshake
192
+ def handshake
193
+ "HTTP/1.1 101 Switching Protocols\r\n" <<
194
+ "Connection: Upgrade\r\n" <<
195
+ "Upgrade: WebSocket\r\n" <<
196
+ if hixie_75?
197
+ "WebSocket-Location: #{location}\r\n" <<
198
+ "WebSocket-Origin: #{origin}\r\n"
199
+ elsif hixie_76?
200
+ "Sec-WebSocket-Location: #{location}\r\n" <<
201
+ "Sec-WebSocket-Origin: #{origin}\r\n"
202
+ else
203
+ "Sec-WebSocket-Accept: #{challenge_response}\r\n"
204
+ end <<
205
+ (protocol ? "Sec-WebSocket-Protocol: #{protocol}\r\n" : "") <<
206
+ "\r\n" <<
207
+ (if hixie_76? then challenge_response else "" end)
208
+ end
209
+
210
+ def handshake!
211
+ if hixie_76?
212
+ [key1, key2].each { |key| raise WebSocketProtocolError, "Invalid key: #{key}" if key >= 2**32 }
213
+ raise WebSocketProtocolError, "Invalid challenge: #{key3}" if key3.length < 8
214
+ end
215
+
216
+ send_data handshake
217
+
218
+ @state = :handshook
219
+
220
+ EM.next_tick { callback :on_handshake, self rescue error! "Error in handshake callback" }
221
+ rescue
222
+ error! "Error during WebSocket connection handshake"
223
+ end
224
+
225
+ def receive_data data
226
+ @buffer << data
227
+
228
+ EM.next_tick { process_frame } if @state == :handshook
229
+ rescue
230
+ error! "Error while receiving WebSocket data"
231
+ end
232
+
233
+ def mask payload, mask_key
234
+ payload.unpack("C*").map.with_index do |byte, index|
235
+ byte ^ mask_key[index % 4]
236
+ end.pack("C*")
237
+ end
238
+
239
+ def process_frame
240
+ if hixie_75? or hixie_76?
241
+ if @buffer.length >= 1
242
+ if @buffer[0].ord < 0x7f
243
+ if ending = @buffer.index("\xff")
244
+ frame = @buffer.slice! 0..ending
245
+ message = frame[1..-2]
246
+
247
+ EM.next_tick { receive_message message }
248
+
249
+ # There might be more frames to process
250
+ EM.next_tick { process_frame }
251
+ elsif @buffer.length > MAX_BUFFER_LENGTH
252
+ raise WebSocketProtocolError, "Maximum buffer length (#{MAX_BUFFER_LENGTH}) exceeded: #{@buffer.length}"
253
+ end
254
+ elsif @buffer[0] == "\xff"
255
+ if @buffer.length > 1
256
+ if @buffer[1] == "\x00"
257
+ @buffer.slice! 0..1
258
+
259
+ EM.next_tick { finish! }
260
+ else
261
+ raise WebSocketProtocolError, "Incorrect finish frame length: #{@buffer[1].inspect}"
262
+ end
263
+ end
264
+ else
265
+ raise WebSocketProtocolError, "Unknown frame type: #{@buffer[0].inspect}"
266
+ end
267
+ end
268
+ else
269
+ @frame_state ||= :opcode
270
+
271
+ if @frame_state == :opcode
272
+ return unless @buffer.length >= 2
273
+
274
+ bytes = @buffer.slice!(0...2).unpack("C*")
275
+
276
+ @opcode = bytes[0] & 0x0f
277
+ @fin = (bytes[0] & 0x80) != 0
278
+ @payload_length = bytes[1] & 0x7f
279
+ @masked = (bytes[1] & 0x80) != 0
280
+
281
+ return error! "Received unmasked data" unless @masked
282
+
283
+ if @payload_length == 126
284
+ @frame_state = :payload_2
285
+ elsif @payload_length == 127
286
+ @frame_state = :payload_8
287
+ else
288
+ @frame_state = :payload
289
+ end
290
+
291
+ elsif @frame_state == :payload_2
292
+ return unless @buffer.length >= 2
293
+
294
+ @payload_length = @buffer.slice!(0...2).unpack("n")[0]
295
+
296
+ @frame_state = :mask
297
+
298
+ elsif @frame_state == :payload_8
299
+ return unless @buffer.length >= 8
300
+
301
+ (high, low) = @buffer.slice!(0...8).unpack("NN")
302
+ @payload_length = high * (2 ** 32) + low
303
+
304
+ @frame_state = :mask
305
+
306
+ elsif @frame_state == :mask
307
+ return unless @buffer.length >= 4
308
+
309
+ bytes = @buffer[(offset)...(offset += 4)]
310
+ @mask_key = bytes.unpack("C*")
311
+
312
+ @frame_state = :payload
313
+
314
+ elsif @frame_state == :payload
315
+ return unless @buffer.length >= @payload_length
316
+
317
+ payload = @buffer.slice!(0...@payload_length)
318
+ payload = mask(payload, @mask_key)
319
+
320
+ if @opcode == OPCODE_TEXT
321
+ message = payload.force_encoding("UTF-8") if payload.respond_to? :force_encoding
322
+ EM.next_tick { receive_message payload }
323
+ elsif @opcode == OPCODE_CLOSE
324
+ EM.next_tick { finish! }
325
+ else
326
+ error! "Unsupported opcode: %d" % @opcode
327
+ end
328
+
329
+ @frame_state = nil
330
+ @opcode = @fin = @payload_length = @masked = nil
331
+ end
332
+ end
333
+ rescue
334
+ error! "Error while processing WebSocket frames"
335
+ end
336
+
337
+ def receive_message message
338
+ EM.next_tick { callback :on_message, self, message rescue error! "Error in message callback" }
339
+ end
340
+
341
+ # This is for post-hixie-76 versions only
342
+ def send_frame opcode, payload="", masked=false
343
+ payload = payload.dup.force_encoding("ASCII-8BIT") if payload.respond_to? :force_encoding
344
+ payload_length = payload.bytesize
345
+
346
+ # We don't support continuations (yet), so always send fin
347
+ fin_byte = 0x80
348
+ send_data [fin_byte | opcode].pack("C")
349
+
350
+ # We shouldn't be sending mask, we're a server only
351
+ masked_byte = masked ? 0x80 : 0x00
352
+
353
+ if payload_length <= 125
354
+ send_data [masked_byte | payload_length].pack("C")
355
+
356
+ elsif payload_length < 2 ** 16
357
+ send_data [masked_byte | 126].pack("C")
358
+ send_data [payload_length].pack("n")
359
+
360
+ else
361
+ send_data [masked_byte | 127].pack("C")
362
+ send_data [payload_length / (2 ** 32), payload_length % (2 ** 32)].pack("NN")
363
+ end
364
+
365
+ if payload_length
366
+ if masked
367
+ mask_key = Array.new(4) { rand(256) }.pack("C*")
368
+ send_data mask_key
369
+ payload = mask payload, mask_key
370
+ end
371
+
372
+ send_data payload
373
+ end
374
+ end
375
+
376
+ def send_message message
377
+ if hixie_75? or hixie_76?
378
+ send_data "\x00#{message}\xff"
379
+ else
380
+ send_frame OPCODE_TEXT, message
381
+ end
382
+ end
383
+
384
+ # Finish the connection read for closing
385
+ def finish!
386
+ if hixie_75? or hixie_76?
387
+ send_data "\xff\x00"
388
+ else
389
+ send_frame OPCODE_CLOSE
390
+ end
391
+
392
+ EM.next_tick { callback(:on_finish, self) rescue error! "Error in finish callback" }
393
+ EM.next_tick { close_connection_after_writing }
394
+
395
+ @state = :finished
396
+ rescue
397
+ error! "Error finishing WebSocket connection"
398
+ end
399
+
400
+ # Make sure we call the on_close callbacks when the connection
401
+ # disappears
402
+ def unbind
403
+ EM.next_tick { callback(:on_close, self) rescue error! "Error in close callback" }
404
+ @state = :closed
405
+ rescue
406
+ error! "Error closing WebSocket connection"
407
+ end
408
+
409
+ def error! message=nil, callback=true
410
+ log message unless message.nil?
411
+ log_error # Logs the exception itself
412
+
413
+ # Allow error messages to be handled, maybe
414
+ # but only if this error was not caused by the error callback
415
+ if callback
416
+ EM.next_tick { callback(:on_error, self) rescue error! "Error in error callback", true }
417
+ end
418
+
419
+ # Try to finish and close nicely.
420
+ EM.next_tick { finish! } unless [:finished, :closed, :error].include? @state
421
+
422
+ # We're closed!
423
+ @state = :error
424
+ end
425
+ end
426
+
427
+ CONNECTION = 'HTTP_CONNECTION'.freeze
428
+ UPGRADE = 'HTTP_UPGRADE'.freeze
429
+ SKINNY_WEBSOCKET = 'skinny.websocket'.freeze
430
+
431
+ UPGRADE_REGEXP = /\bupgrade\b/i.freeze
432
+ WEBSOCKET_REGEXP = /\bwebsocket\b/i.freeze
433
+
434
+ module Helpers
435
+ def websocket?
436
+ env[CONNECTION] =~ UPGRADE_REGEXP && env[UPGRADE] =~ WEBSOCKET_REGEXP
437
+ end
438
+
439
+ def websocket options={}, &block
440
+ env[SKINNY_WEBSOCKET] ||= begin
441
+ raise RuntimerError, "Not a WebSocket request" unless websocket?
442
+ options[:on_message] = block if block_given?
443
+ Websocket.from_env(env, options)
444
+ end
445
+ end
446
+
447
+ def websocket! options={}, &block
448
+ websocket(options, &block).start!
449
+ end
450
+ end
451
+ end
@@ -0,0 +1,3 @@
1
+ module Skinny
2
+ VERSION = "0.2.4"
3
+ end
data/skinny.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'skinny/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tuttinator-skinny"
8
+ spec.version = Skinny::VERSION
9
+ spec.summary = "Thin WebSockets"
10
+ spec.description = "Simple, upgradable WebSockets for Thin."
11
+ spec.summary = spec.description
12
+ spec.author = ["Caleb Tutty", "Samuel Cochran"]
13
+ spec.email = ["caleb@prettymint.co.nz", "sj26@sj26.com"]
14
+ spec.homepage = "http://github.com/sj26/skinny"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.extra_rdoc_files = ["README.md", "LICENSE"]
22
+
23
+ spec.add_dependency "eventmachine", "~> 1.0.0"
24
+ spec.add_dependency "thin", "> 1.5.0", "< 1.7.0"
25
+
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rdoc"
28
+ spec.add_development_dependency 'rspec'
29
+ end
30
+
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tuttinator-skinny
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ platform: ruby
6
+ authors:
7
+ - Caleb Tutty
8
+ - Samuel Cochran
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: 1.0.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: 1.0.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: thin
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '>'
33
+ - !ruby/object:Gem::Version
34
+ version: 1.5.0
35
+ - - <
36
+ - !ruby/object:Gem::Version
37
+ version: 1.7.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - '>'
43
+ - !ruby/object:Gem::Version
44
+ version: 1.5.0
45
+ - - <
46
+ - !ruby/object:Gem::Version
47
+ version: 1.7.0
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rdoc
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: rspec
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ description: Simple, upgradable WebSockets for Thin.
91
+ email:
92
+ - caleb@prettymint.co.nz
93
+ - sj26@sj26.com
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files:
97
+ - README.md
98
+ - LICENSE
99
+ files:
100
+ - .gitignore
101
+ - Gemfile
102
+ - LICENSE
103
+ - README.md
104
+ - Rakefile
105
+ - lib/skinny.rb
106
+ - lib/skinny/version.rb
107
+ - skinny.gemspec
108
+ homepage: http://github.com/sj26/skinny
109
+ licenses: []
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.0.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Simple, upgradable WebSockets for Thin.
131
+ test_files: []
132
+ has_rdoc: