skinny 0.1.0 → 0.1.2

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