tuttinator-skinny 0.2.4

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