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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +62 -0
- data/Rakefile +6 -0
- data/lib/skinny.rb +451 -0
- data/lib/skinny/version.rb +3 -0
- data/skinny.gemspec +30 -0
- metadata +132 -0
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
data/Gemfile
ADDED
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
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
|
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:
|