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.
Files changed (4) hide show
  1. data/README.md +4 -0
  2. data/VERSION +1 -1
  3. data/lib/skinny.rb +97 -55
  4. 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.0
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
- # Steal the connection
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(thin_connection.detach, self, env, options)
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
- EM.next_tick { callback :on_open, self }
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 protocol= value
102
- @env['HTTP_SEC_WEBSOCKET_PROTOCOL'] = value
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 = @env["HTTP_SEC_WEBSOCKET_KEY#{i}"]
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
- @env.has_key? 'HTTP_SEC_WEBSOCKET_KEY1'
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: 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']) +
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
- @handshook = true
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] == "\x00"
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
- error! "Maximum buffer length (#{MAX_BUFFER_LENGTH}) exceeded: #{@buffer.length}"
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
- error! "Incorrect finish frame length: #{@buffer[1].inspect}"
204
+ raise WebSocketProtocolError, "Incorrect finish frame length: #{@buffer[1].inspect}"
179
205
  end
180
206
  end
181
207
  else
182
- error! "Unknown frame type: #{@buffer[0].inspect}"
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
- 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
-
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 RequestHelpers
262
+ module Helpers
222
263
  def websocket?
223
- @env['HTTP_CONNECTION'] == 'Upgrade' && @env['HTTP_UPGRADE'] == 'WebSocket'
264
+ env['HTTP_CONNECTION'] == 'Upgrade' && env['HTTP_UPGRADE'] == 'WebSocket'
224
265
  end
225
266
 
226
- def websocket(options={})
227
- @env['skinny.websocket'] ||= begin
267
+ def websocket(options={}, &block)
268
+ env['skinny.websocket'] ||= begin
228
269
  raise RuntimerError, "Not a WebSocket request" unless websocket?
229
- Websocket.from_env(@env, options)
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: 27
4
+ hash: 31
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
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-10-28 00:00:00 +08:00
18
+ date: 2010-11-01 00:00:00 +08:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency