skinny 0.1.0 → 0.1.2
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.
- data/README.md +4 -0
- data/VERSION +1 -1
- data/lib/skinny.rb +97 -55
- metadata +4 -4
data/README.md
CHANGED
@@ -13,6 +13,10 @@ More details coming soon.
|
|
13
13
|
More comprehensive examples will be coming soon. Here's a really
|
14
14
|
simple, not-yet-optimised example I'm using at the moment:
|
15
15
|
|
16
|
+
class Sinatra::Request
|
17
|
+
include Skinny::Helpers
|
18
|
+
end
|
19
|
+
|
16
20
|
module MailCatcher
|
17
21
|
class Web < Sinatra::Base
|
18
22
|
get '/messages' do
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.2
|
data/lib/skinny.rb
CHANGED
@@ -37,22 +37,28 @@ module Skinny
|
|
37
37
|
|
38
38
|
class WebSocketError < RuntimeError; end
|
39
39
|
class WebSocketProtocolError < WebSocketError; end
|
40
|
-
|
40
|
+
|
41
|
+
# We need to be really careful not to throw an exception too high
|
42
|
+
# or we'll kill the server.
|
41
43
|
class Websocket < EventMachine::Connection
|
42
44
|
include Callbacks
|
45
|
+
include Thin::Logging
|
43
46
|
|
44
47
|
define_callback :on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close
|
45
48
|
|
46
49
|
# 4mb is almost too generous, imho.
|
47
50
|
MAX_BUFFER_LENGTH = 2 ** 32
|
48
|
-
|
51
|
+
|
52
|
+
# Create a new WebSocket from a Thin::Request environment
|
49
53
|
def self.from_env env, options={}
|
50
|
-
#
|
54
|
+
# Pull the connection out of the env
|
51
55
|
thin_connection = env[Thin::Request::ASYNC_CALLBACK].receiver
|
56
|
+
# Steal the IO
|
57
|
+
io = thin_connection.detach
|
52
58
|
# We have all the events now, muahaha
|
53
|
-
EM.attach(
|
59
|
+
EM.attach(io, self, env, options)
|
54
60
|
end
|
55
|
-
|
61
|
+
|
56
62
|
def initialize env, options={}
|
57
63
|
@env = env.dup
|
58
64
|
@buffer = ''
|
@@ -62,8 +68,14 @@ module Skinny
|
|
62
68
|
send name, &options.delete(name) if options.has_key?(name)
|
63
69
|
end
|
64
70
|
raise ArgumentError, "Unknown options: #{options.inspect}" unless options.empty?
|
65
|
-
|
66
|
-
|
71
|
+
end
|
72
|
+
|
73
|
+
# Connection is now open
|
74
|
+
def post_init
|
75
|
+
EM.next_tick { callback :on_open, self rescue error! "Error in open callback" }
|
76
|
+
@state = :open
|
77
|
+
rescue
|
78
|
+
error! "Error opening connection"
|
67
79
|
end
|
68
80
|
|
69
81
|
# Return an async response -- stops Thin doing anything with connection.
|
@@ -74,6 +86,7 @@ module Skinny
|
|
74
86
|
# Arrayify self into a response tuple
|
75
87
|
alias :to_a :response
|
76
88
|
|
89
|
+
# Start the websocket connection
|
77
90
|
def start!
|
78
91
|
# Steal any remaining data from rack.input
|
79
92
|
@buffer = @env[Thin::Request::RACK_INPUT].read + @buffer
|
@@ -82,29 +95,37 @@ module Skinny
|
|
82
95
|
@env.delete Thin::Request::RACK_INPUT
|
83
96
|
@env.delete Thin::Request::ASYNC_CALLBACK
|
84
97
|
@env.delete Thin::Request::ASYNC_CLOSE
|
98
|
+
|
99
|
+
# Pull out the details we care about
|
100
|
+
@origin ||= @env['HTTP_ORIGIN']
|
101
|
+
@location ||= "ws#{secure? ? 's' : ''}://#{@env['HTTP_HOST']}#{@env['REQUEST_PATH']}"
|
102
|
+
@protocol ||= @env['HTTP_SEC_WEBSOCKET_PROTOCOL']
|
85
103
|
|
86
|
-
EM.next_tick { callback :on_start, self }
|
104
|
+
EM.next_tick { callback :on_start, self rescue error! "Error in start callback" }
|
87
105
|
|
88
106
|
# Queue up the actual handshake
|
89
107
|
EM.next_tick method :handshake!
|
108
|
+
|
109
|
+
@state = :started
|
90
110
|
|
91
111
|
# Return self so we can be used as a response
|
92
112
|
self
|
93
113
|
rescue
|
94
|
-
error!
|
95
|
-
end
|
96
|
-
|
97
|
-
def protocol
|
98
|
-
@env['HTTP_SEC_WEBSOCKET_PROTOCOL']
|
114
|
+
error! "Error starting connection"
|
99
115
|
end
|
116
|
+
|
117
|
+
attr_reader :env
|
118
|
+
attr_accessor :origin, :location, :protocol
|
100
119
|
|
101
|
-
def
|
102
|
-
@env['
|
120
|
+
def secure?
|
121
|
+
@env['HTTPS'] == 'on' or
|
122
|
+
@env['HTTP_X_FORWARDED_PROTO'] == 'https' or
|
123
|
+
@env['rack.url_scheme'] == 'https'
|
103
124
|
end
|
104
|
-
|
125
|
+
|
105
126
|
[1, 2].each do |i|
|
106
|
-
define_method "key#{i}" do
|
107
|
-
key =
|
127
|
+
define_method :"key#{i}" do
|
128
|
+
key = env["HTTP_SEC_WEBSOCKET_KEY#{i}"]
|
108
129
|
key.scan(/[0-9]/).join.to_i / key.count(' ')
|
109
130
|
end
|
110
131
|
end
|
@@ -114,7 +135,7 @@ module Skinny
|
|
114
135
|
end
|
115
136
|
|
116
137
|
def challenge?
|
117
|
-
|
138
|
+
env.has_key? 'HTTP_SEC_WEBSOCKET_KEY1'
|
118
139
|
end
|
119
140
|
|
120
141
|
def challenge
|
@@ -125,48 +146,53 @@ module Skinny
|
|
125
146
|
Digest::MD5.digest(challenge)
|
126
147
|
end
|
127
148
|
|
149
|
+
# Generate the handshake
|
128
150
|
def handshake
|
129
151
|
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
|
130
152
|
"Connection: Upgrade\r\n" +
|
131
153
|
"Upgrade: WebSocket\r\n" +
|
132
|
-
"Sec-WebSocket-Location:
|
133
|
-
"Sec-WebSocket-Origin: #{
|
134
|
-
("Sec-WebSocket-Protocol: #{
|
154
|
+
"Sec-WebSocket-Location: #{location}\r\n" +
|
155
|
+
"Sec-WebSocket-Origin: #{origin}\r\n" +
|
156
|
+
(protocol ? "Sec-WebSocket-Protocol: #{protocol}\r\n" : "") +
|
135
157
|
"\r\n" +
|
136
158
|
"#{challenge_response}"
|
137
159
|
end
|
138
|
-
|
160
|
+
|
139
161
|
def handshake!
|
140
162
|
[key1, key2].each { |key| raise WebSocketProtocolError, "Invalid key: #{key}" if key >= 2**32 }
|
163
|
+
|
141
164
|
# XXX: Should we wait for 8 bytes?
|
142
165
|
raise WebSocketProtocolError, "Invalid challenge: #{key3}" if key3.length < 8
|
143
166
|
|
144
167
|
send_data handshake
|
145
|
-
@
|
168
|
+
@state = :handshook
|
146
169
|
|
147
|
-
EM.next_tick { callback :on_handshake, self }
|
170
|
+
EM.next_tick { callback :on_handshake, self rescue error! "Error in handshake callback" }
|
148
171
|
rescue
|
149
|
-
error!
|
172
|
+
error! "Error during WebSocket connection handshake"
|
150
173
|
end
|
151
|
-
|
174
|
+
|
152
175
|
def receive_data data
|
153
176
|
@buffer += data
|
154
177
|
|
155
|
-
EM.next_tick { process_frame } if @handshook
|
178
|
+
EM.next_tick { process_frame } if @state == :handshook
|
156
179
|
rescue
|
157
|
-
error!
|
180
|
+
error! "Error while receiving WebSocket data"
|
158
181
|
end
|
159
|
-
|
182
|
+
|
160
183
|
def process_frame
|
161
184
|
if @buffer.length >= 1
|
162
|
-
if @buffer[0]
|
185
|
+
if @buffer[0].ord < 0x7f
|
163
186
|
if ending = @buffer.index("\xff")
|
164
187
|
frame = @buffer.slice! 0..ending
|
165
188
|
message = frame[1..-2]
|
166
189
|
|
167
190
|
EM.next_tick { receive_message message }
|
191
|
+
|
192
|
+
# There might be more frames to process
|
193
|
+
EM.next_tick { process_frame }
|
168
194
|
elsif @buffer.length > MAX_BUFFER_LENGTH
|
169
|
-
|
195
|
+
raise WebSocketProtocolError, "Maximum buffer length (#{MAX_BUFFER_LENGTH}) exceeded: #{@buffer.length}"
|
170
196
|
end
|
171
197
|
elsif @buffer[0] == "\xff"
|
172
198
|
if @buffer.length > 1
|
@@ -175,17 +201,19 @@ module Skinny
|
|
175
201
|
|
176
202
|
EM.next_tick { finish! }
|
177
203
|
else
|
178
|
-
|
204
|
+
raise WebSocketProtocolError, "Incorrect finish frame length: #{@buffer[1].inspect}"
|
179
205
|
end
|
180
206
|
end
|
181
207
|
else
|
182
|
-
|
208
|
+
raise WebSocketProtocolError, "Unknown frame type: #{@buffer[0].inspect}"
|
183
209
|
end
|
184
210
|
end
|
211
|
+
rescue
|
212
|
+
error! "Error while processing WebSocket frames"
|
185
213
|
end
|
186
214
|
|
187
215
|
def receive_message message
|
188
|
-
EM.next_tick { callback :on_message, self, message }
|
216
|
+
EM.next_tick { callback :on_message, self, message rescue error! "Error in message callback" }
|
189
217
|
end
|
190
218
|
|
191
219
|
def frame_message message
|
@@ -196,42 +224,56 @@ module Skinny
|
|
196
224
|
send_data frame_message(message)
|
197
225
|
end
|
198
226
|
|
199
|
-
|
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
|
-
|
227
|
+
# Finish the connection read for closing
|
206
228
|
def finish!
|
207
229
|
send_data "\xff\x00"
|
208
|
-
close_connection_after_writing
|
209
|
-
@finished = true
|
210
230
|
|
211
|
-
EM.next_tick { callback :on_finish, self }
|
231
|
+
EM.next_tick { callback :on_finish, self rescue error! "Error in finish callback" }
|
232
|
+
EM.next_tick { close_connection_after_writing }
|
233
|
+
|
234
|
+
@state = :finished
|
212
235
|
rescue
|
213
|
-
error!
|
236
|
+
error! "Error finishing WebSocket connection"
|
214
237
|
end
|
215
|
-
|
238
|
+
|
239
|
+
# Make sure we call the on_close callbacks when the connection
|
240
|
+
# disappears
|
216
241
|
def unbind
|
217
|
-
EM.next_tick { callback :on_close, self }
|
242
|
+
EM.next_tick { callback :on_close, self rescue error! "Error in close callback" }
|
243
|
+
@state = :closed
|
244
|
+
rescue
|
245
|
+
error! "Error closing WebSocket connection"
|
246
|
+
end
|
247
|
+
|
248
|
+
def error! message=nil
|
249
|
+
log message unless message.nil?
|
250
|
+
log_error
|
251
|
+
|
252
|
+
# Allow error messages to be handled, maybe
|
253
|
+
EM.next_tick { callback :on_error, self rescue error! "Error in error callback" }
|
254
|
+
|
255
|
+
# Try to finish and close nicely.
|
256
|
+
EM.next_tick { finish! } unless [:finished, :closed, :error].include? @state
|
257
|
+
|
258
|
+
@state = :error
|
218
259
|
end
|
219
260
|
end
|
220
261
|
|
221
|
-
module
|
262
|
+
module Helpers
|
222
263
|
def websocket?
|
223
|
-
|
264
|
+
env['HTTP_CONNECTION'] == 'Upgrade' && env['HTTP_UPGRADE'] == 'WebSocket'
|
224
265
|
end
|
225
266
|
|
226
|
-
def websocket(options={})
|
227
|
-
|
267
|
+
def websocket(options={}, &block)
|
268
|
+
env['skinny.websocket'] ||= begin
|
228
269
|
raise RuntimerError, "Not a WebSocket request" unless websocket?
|
229
|
-
|
270
|
+
options[:on_message] = block if block_given?
|
271
|
+
Websocket.from_env(env, options)
|
230
272
|
end
|
231
273
|
end
|
232
274
|
|
233
|
-
def websocket!(options={})
|
234
|
-
websocket(options).start!
|
275
|
+
def websocket!(options={}, &block)
|
276
|
+
websocket(options, &block).start!
|
235
277
|
end
|
236
278
|
end
|
237
279
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: skinny
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 31
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 2
|
10
|
+
version: 0.1.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Samuel Cochran
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-11-01 00:00:00 +08:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|