cramp 0.14.1 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,6 +10,7 @@ require 'active_support/core_ext/module/attribute_accessors'
10
10
  require 'active_support/core_ext/kernel/reporting'
11
11
  require 'active_support/concern'
12
12
  require 'active_support/core_ext/hash/indifferent_access'
13
+ require 'active_support/core_ext/hash/except'
13
14
  require 'active_support/buffered_logger'
14
15
 
15
16
  require 'rack'
@@ -19,13 +20,14 @@ if RUBY_VERSION >= '1.9.1'
19
20
  end
20
21
 
21
22
  module Cramp
22
- VERSION = '0.14.1'
23
+ VERSION = '0.15'
23
24
 
24
25
  mattr_accessor :logger
25
26
 
26
27
  autoload :Action, "cramp/action"
27
28
  autoload :Websocket, "cramp/websocket"
28
29
  autoload :WebsocketExtension, "cramp/websocket/extension"
30
+ autoload :Protocol10FrameParser, "cramp/websocket/protocol10_frame_parser"
29
31
  autoload :SSE, "cramp/sse"
30
32
  autoload :LongPolling, "cramp/long_polling"
31
33
  autoload :Body, "cramp/body"
@@ -34,5 +36,6 @@ module Cramp
34
36
  autoload :Abstract, "cramp/abstract"
35
37
  autoload :Callbacks, "cramp/callbacks"
36
38
  autoload :FiberPool, "cramp/fiber_pool"
39
+ autoload :ExceptionHandler, "cramp/exception_handler"
37
40
  autoload :TestCase, "cramp/test_case"
38
41
  end
@@ -16,9 +16,9 @@ module Cramp
16
16
 
17
17
  def initialize(env)
18
18
  @env = env
19
- @env['websocket.receive_callback'] = method(:_on_data_receive)
20
-
21
19
  @finished = false
20
+
21
+ @_state = :init
22
22
  end
23
23
 
24
24
  def process
@@ -30,15 +30,21 @@ module Cramp
30
30
 
31
31
  def continue
32
32
  init_async_body
33
+ send_headers
33
34
 
34
- status, headers = respond_with
35
- send_initial_response(status, headers, @body)
36
-
35
+ @_state = :started
37
36
  EM.next_tick { on_start }
38
37
  end
39
38
 
40
- def respond_with
41
- [200, {'Content-Type' => 'text/html'}]
39
+ def send_headers
40
+ status, headers = build_headers
41
+ send_initial_response(status, headers, @body)
42
+ rescue StandardError, LoadError, SyntaxError => exception
43
+ handle_exception(exception)
44
+ end
45
+
46
+ def build_headers
47
+ respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
42
48
  end
43
49
 
44
50
  def init_async_body
@@ -55,8 +61,10 @@ module Cramp
55
61
  end
56
62
 
57
63
  def finish
64
+ @body.succeed if is_finishable?
65
+ ensure
66
+ @_state = :finished
58
67
  @finished = true
59
- @body.succeed
60
68
  end
61
69
 
62
70
  def send_initial_response(response_status, response_headers, response_body)
@@ -82,5 +90,12 @@ module Cramp
82
90
  def route_params
83
91
  @env['router.params'] || @env['usher.params']
84
92
  end
93
+
94
+ private
95
+
96
+ def is_finishable?
97
+ !finished? && @body && !@body.closed?
98
+ end
99
+
85
100
  end
86
101
  end
@@ -3,25 +3,44 @@ module Cramp
3
3
  include PeriodicTimer
4
4
  include KeepConnectionAlive
5
5
 
6
+ def initialize(env)
7
+ super
8
+ @env['websocket.receive_callback'] = method(:_on_data_receive)
9
+ end
10
+
6
11
  protected
7
12
 
8
13
  def render(body, *args)
9
14
  send(:"render_#{transport}", body, *args)
10
15
  end
11
16
 
12
- def send_initial_response(*)
17
+ def send_initial_response(status, headers, body)
13
18
  case transport
14
19
  when :long_polling
15
- # Dont send no initial response
20
+ # Dont send no initial response. Just cache it for later.
21
+ @_lp_status = status
22
+ @_lp_headers = headers
16
23
  else
17
24
  super
18
25
  end
19
26
  end
20
27
 
21
- def respond_with
28
+ class_attribute :default_sse_headers
29
+ self.default_sse_headers = {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive'}
30
+
31
+ class_attribute :default_chunked_headers
32
+ self.default_chunked_headers = {'Transfer-Encoding' => 'chunked', 'Connection' => 'keep-alive'}
33
+
34
+ def build_headers
22
35
  case transport
23
36
  when :sse
24
- [200, {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive'}]
37
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
38
+ [status, headers.merge(self.default_sse_headers)]
39
+ when :chunked
40
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {}]
41
+ headers['Content-Type'] ||= 'text/html'
42
+
43
+ [status, headers.merge(self.default_chunked_headers)]
25
44
  else
26
45
  super
27
46
  end
@@ -32,10 +51,9 @@ module Cramp
32
51
  end
33
52
 
34
53
  def render_long_polling(data, *)
35
- status, headers = respond_with
36
- headers['Content-Length'] = data.size.to_s
54
+ @_lp_headers['Content-Length'] = data.size.to_s
37
55
 
38
- send_response(status, headers, @body)
56
+ send_response(@_lp_status, @_lp_headers, @body)
39
57
  @body.call(data)
40
58
 
41
59
  finish
@@ -52,7 +70,21 @@ module Cramp
52
70
  end
53
71
 
54
72
  def render_websocket(body, *)
55
- data = ["\x00", body, "\xFF"].map(&method(:encode)) * ''
73
+ if websockets_protocol_10?
74
+ data = encode(protocol10_parser.send_text_frame(body), 'BINARY')
75
+ else
76
+ data = ["\x00", body, "\xFF"].map(&method(:encode)) * ''
77
+ end
78
+
79
+ @body.call(data)
80
+ end
81
+
82
+ CHUNKED_TERM = "\r\n"
83
+ CHUNKED_TAIL = "0#{CHUNKED_TERM}#{CHUNKED_TERM}"
84
+
85
+ def render_chunked(body, *)
86
+ data = [Rack::Utils.bytesize(body).to_s(16), CHUNKED_TERM, body, CHUNKED_TERM].join
87
+
56
88
  @body.call(data)
57
89
  end
58
90
 
@@ -65,5 +97,24 @@ module Cramp
65
97
  string.respond_to?(:force_encoding) ? string.force_encoding(encoding) : string
66
98
  end
67
99
 
100
+ protected
101
+
102
+ def finish
103
+ case transport
104
+ when :chunked
105
+ @body.call(CHUNKED_TAIL) if is_finishable?
106
+ end
107
+
108
+ super
109
+ end
110
+
111
+ def websockets_protocol_10?
112
+ [8, 9, 10].include?(@env['HTTP_SEC_WEBSOCKET_VERSION'].to_i)
113
+ end
114
+
115
+ def protocol10_parser
116
+ @protocol10_parser ||= Protocol10FrameParser.new
117
+ end
118
+
68
119
  end
69
120
  end
@@ -53,15 +53,55 @@ module Cramp
53
53
  end
54
54
 
55
55
  def callback_wrapper
56
- EM.next_tick { yield }
56
+ EM.next_tick do
57
+ begin
58
+ yield
59
+ rescue StandardError, LoadError, SyntaxError => exception
60
+ handle_exception(exception)
61
+ end
62
+ end
57
63
  end
58
64
 
59
65
  def _on_data_receive(data)
66
+ websockets_protocol_10? ? _receive_protocol10_data(data) : _receive_protocol76_data(data)
67
+ end
68
+
69
+ protected
70
+
71
+ def _receive_protocol10_data(data)
72
+ protocol10_parser.data << data
73
+
74
+ messages = @protocol10_parser.process_data
75
+ messages.each do |type, content|
76
+ _invoke_data_callbacks(content) if type == :text
77
+ end
78
+ end
79
+
80
+ def _receive_protocol76_data(data)
60
81
  data = data.split(/\000([^\377]*)\377/).select{|d| !d.empty? }.collect{|d| d.gsub(/^\x00|\xff$/, '') }
82
+ data.each {|message| _invoke_data_callbacks(message) }
83
+ end
84
+
85
+ def _invoke_data_callbacks(message)
61
86
  self.class.on_data_callbacks.each do |callback|
62
- data.each do |message|
63
- callback_wrapper { send(callback, message) }
64
- end
87
+ callback_wrapper { send(callback, message) }
88
+ end
89
+ end
90
+
91
+ def handle_exception(exception)
92
+ handler = ExceptionHandler.new(@env, exception)
93
+
94
+ # Log the exception
95
+ unless ENV['RACK_ENV'] == 'test'
96
+ exception_body = handler.dump_exception
97
+ Cramp.logger ? Cramp.logger.error(exception_body) : $stderr.puts(exception_body)
98
+ end
99
+
100
+ case @_state
101
+ when :init
102
+ halt 500, {"Content-Type" => 'text/html'}, ENV['RACK_ENV'] == 'development' ? handler.pretty : 'Something went wrong'
103
+ else
104
+ finish
65
105
  end
66
106
  end
67
107
 
@@ -0,0 +1,357 @@
1
+ # Based on Rack::ShowExceptions
2
+ #
3
+ # Copyright (c) 2007, 2008, 2009, 2010 Christian Neukirchen <purl.org/net/chneukirchen>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to
7
+ # deal in the Software without restriction, including without limitation the
8
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ # sell copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
+ # THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'erb'
23
+ require 'ostruct'
24
+
25
+ module Cramp
26
+ class ExceptionHandler
27
+
28
+ attr_reader :env, :exception
29
+
30
+ def initialize(env, exception)
31
+ @env = env
32
+ @exception = exception
33
+ @template = ERB.new(TEMPLATE)
34
+ end
35
+
36
+ def dump_exception
37
+ string = "#{exception.class}: #{exception.message}\n"
38
+ string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
39
+ string
40
+ end
41
+
42
+ def pretty
43
+ req = Rack::Request.new(env)
44
+
45
+ # This double assignment is to prevent an "unused variable" warning on
46
+ # Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
47
+ path = path = (req.script_name + req.path_info).squeeze("/")
48
+
49
+ # This double assignment is to prevent an "unused variable" warning on
50
+ # Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
51
+ frames = frames = exception.backtrace.map { |line|
52
+ frame = OpenStruct.new
53
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
54
+ frame.filename = $1
55
+ frame.lineno = $2.to_i
56
+ frame.function = $4
57
+
58
+ begin
59
+ lineno = frame.lineno-1
60
+ lines = ::File.readlines(frame.filename)
61
+ frame.pre_context_lineno = [lineno-CONTEXT, 0].max
62
+ frame.pre_context = lines[frame.pre_context_lineno...lineno]
63
+ frame.context_line = lines[lineno].chomp
64
+ frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
65
+ frame.post_context = lines[lineno+1..frame.post_context_lineno]
66
+ rescue
67
+ end
68
+
69
+ frame
70
+ else
71
+ nil
72
+ end
73
+ }.compact
74
+
75
+ [@template.result(binding)]
76
+ end
77
+
78
+ def h(obj) # :nodoc:
79
+ case obj
80
+ when String
81
+ Rack::Utils.escape_html(obj)
82
+ else
83
+ Rack::Utils.escape_html(obj.inspect)
84
+ end
85
+ end
86
+
87
+ # adapted from Django <djangoproject.com>
88
+ # Copyright (c) 2005, the Lawrence Journal-World
89
+ # Used under the modified BSD license:
90
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
91
+ TEMPLATE = <<'HTML'
92
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
93
+ <html lang="en">
94
+ <head>
95
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
96
+ <meta name="robots" content="NONE,NOARCHIVE" />
97
+ <title><%=h exception.class %> at <%=h path %></title>
98
+ <style type="text/css">
99
+ html * { padding:0; margin:0; }
100
+ body * { padding:10px 20px; }
101
+ body * * { padding:0; }
102
+ body { font:small sans-serif; }
103
+ body>div { border-bottom:1px solid #ddd; }
104
+ h1 { font-weight:normal; }
105
+ h2 { margin-bottom:.8em; }
106
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
107
+ h3 { margin:1em 0 .5em 0; }
108
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
109
+ table {
110
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
111
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
112
+ thead th {
113
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
114
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
115
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
116
+ table.vars { margin:5px 0 2px 40px; }
117
+ table.vars td, table.req td { font-family:monospace; }
118
+ table td.code { width:100%;}
119
+ table td.code div { overflow:hidden; }
120
+ table.source th { color:#666; }
121
+ table.source td {
122
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
123
+ ul.traceback { list-style-type:none; }
124
+ ul.traceback li.frame { margin-bottom:1em; }
125
+ div.context { margin: 10px 0; }
126
+ div.context ol {
127
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
128
+ div.context ol li {
129
+ font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
130
+ div.context ol.context-line li { color:black; background-color:#ccc; }
131
+ div.context ol.context-line li span { float: right; }
132
+ div.commands { margin-left: 40px; }
133
+ div.commands a { color:black; text-decoration:none; }
134
+ #summary { background: #ffc; }
135
+ #summary h2 { font-weight: normal; color: #666; }
136
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
137
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }
138
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
139
+ #explanation { background:#eee; }
140
+ #template, #template-not-exist { background:#f6f6f6; }
141
+ #template-not-exist ul { margin: 0 0 0 20px; }
142
+ #traceback { background:#eee; }
143
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
144
+ #summary table { border:none; background:transparent; }
145
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
146
+ #requestinfo h3 { margin-bottom:-1em; }
147
+ .error { background: #ffc; }
148
+ .specific { color:#cc3300; font-weight:bold; }
149
+ </style>
150
+ <script type="text/javascript">
151
+ //<!--
152
+ function getElementsByClassName(oElm, strTagName, strClassName){
153
+ // Written by Jonathan Snook, http://www.snook.ca/jon;
154
+ // Add-ons by Robert Nyman, http://www.robertnyman.com
155
+ var arrElements = (strTagName == "*" && document.all)? document.all :
156
+ oElm.getElementsByTagName(strTagName);
157
+ var arrReturnElements = new Array();
158
+ strClassName = strClassName.replace(/\-/g, "\\-");
159
+ var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
160
+ var oElement;
161
+ for(var i=0; i<arrElements.length; i++){
162
+ oElement = arrElements[i];
163
+ if(oRegExp.test(oElement.className)){
164
+ arrReturnElements.push(oElement);
165
+ }
166
+ }
167
+ return (arrReturnElements)
168
+ }
169
+ function hideAll(elems) {
170
+ for (var e = 0; e < elems.length; e++) {
171
+ elems[e].style.display = 'none';
172
+ }
173
+ }
174
+ window.onload = function() {
175
+ hideAll(getElementsByClassName(document, 'table', 'vars'));
176
+ hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
177
+ hideAll(getElementsByClassName(document, 'ol', 'post-context'));
178
+ }
179
+ function toggle() {
180
+ for (var i = 0; i < arguments.length; i++) {
181
+ var e = document.getElementById(arguments[i]);
182
+ if (e) {
183
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+ function varToggle(link, id) {
189
+ toggle('v' + id);
190
+ var s = link.getElementsByTagName('span')[0];
191
+ var uarr = String.fromCharCode(0x25b6);
192
+ var darr = String.fromCharCode(0x25bc);
193
+ s.innerHTML = s.innerHTML == uarr ? darr : uarr;
194
+ return false;
195
+ }
196
+ //-->
197
+ </script>
198
+ </head>
199
+ <body>
200
+
201
+ <div id="summary">
202
+ <h1><%=h exception.class %> at <%=h path %></h1>
203
+ <h2><%=h exception.message %></h2>
204
+ <table><tr>
205
+ <th>Ruby</th>
206
+ <td>
207
+ <% if first = frames.first %>
208
+ <code><%=h first.filename %></code>: in <code><%=h first.function %></code>, line <%=h frames.first.lineno %>
209
+ <% else %>
210
+ unknown location
211
+ <% end %>
212
+ </td>
213
+ </tr><tr>
214
+ <th>Web</th>
215
+ <td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>
216
+ </tr></table>
217
+
218
+ <h3>Jump to:</h3>
219
+ <ul id="quicklinks">
220
+ <li><a href="#get-info">GET</a></li>
221
+ <li><a href="#post-info">POST</a></li>
222
+ <li><a href="#cookie-info">Cookies</a></li>
223
+ <li><a href="#env-info">ENV</a></li>
224
+ </ul>
225
+ </div>
226
+
227
+ <div id="traceback">
228
+ <h2>Traceback <span>(innermost first)</span></h2>
229
+ <ul class="traceback">
230
+ <% frames.each { |frame| %>
231
+ <li class="frame">
232
+ <code><%=h frame.filename %></code>: in <code><%=h frame.function %></code>
233
+
234
+ <% if frame.context_line %>
235
+ <div class="context" id="c<%=h frame.object_id %>">
236
+ <% if frame.pre_context %>
237
+ <ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
238
+ <% frame.pre_context.each { |line| %>
239
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
240
+ <% } %>
241
+ </ol>
242
+ <% end %>
243
+
244
+ <ol start="<%=h frame.lineno %>" class="context-line">
245
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %><span>...</span></li></ol>
246
+
247
+ <% if frame.post_context %>
248
+ <ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
249
+ <% frame.post_context.each { |line| %>
250
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
251
+ <% } %>
252
+ </ol>
253
+ <% end %>
254
+ </div>
255
+ <% end %>
256
+ </li>
257
+ <% } %>
258
+ </ul>
259
+ </div>
260
+
261
+ <div id="requestinfo">
262
+ <h2>Request information</h2>
263
+
264
+ <h3 id="get-info">GET</h3>
265
+ <% unless req.GET.empty? %>
266
+ <table class="req">
267
+ <thead>
268
+ <tr>
269
+ <th>Variable</th>
270
+ <th>Value</th>
271
+ </tr>
272
+ </thead>
273
+ <tbody>
274
+ <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
275
+ <tr>
276
+ <td><%=h key %></td>
277
+ <td class="code"><div><%=h val.inspect %></div></td>
278
+ </tr>
279
+ <% } %>
280
+ </tbody>
281
+ </table>
282
+ <% else %>
283
+ <p>No GET data.</p>
284
+ <% end %>
285
+
286
+ <h3 id="post-info">POST</h3>
287
+ <% unless req.POST.empty? %>
288
+ <table class="req">
289
+ <thead>
290
+ <tr>
291
+ <th>Variable</th>
292
+ <th>Value</th>
293
+ </tr>
294
+ </thead>
295
+ <tbody>
296
+ <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
297
+ <tr>
298
+ <td><%=h key %></td>
299
+ <td class="code"><div><%=h val.inspect %></div></td>
300
+ </tr>
301
+ <% } %>
302
+ </tbody>
303
+ </table>
304
+ <% else %>
305
+ <p>No POST data.</p>
306
+ <% end %>
307
+
308
+
309
+ <h3 id="cookie-info">COOKIES</h3>
310
+ <% unless req.cookies.empty? %>
311
+ <table class="req">
312
+ <thead>
313
+ <tr>
314
+ <th>Variable</th>
315
+ <th>Value</th>
316
+ </tr>
317
+ </thead>
318
+ <tbody>
319
+ <% req.cookies.each { |key, val| %>
320
+ <tr>
321
+ <td><%=h key %></td>
322
+ <td class="code"><div><%=h val.inspect %></div></td>
323
+ </tr>
324
+ <% } %>
325
+ </tbody>
326
+ </table>
327
+ <% else %>
328
+ <p>No cookie data.</p>
329
+ <% end %>
330
+
331
+ <h3 id="env-info">Rack ENV</h3>
332
+ <table class="req">
333
+ <thead>
334
+ <tr>
335
+ <th>Variable</th>
336
+ <th>Value</th>
337
+ </tr>
338
+ </thead>
339
+ <tbody>
340
+ <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
341
+ <tr>
342
+ <td><%=h key %></td>
343
+ <td class="code"><div><%=h val %></div></td>
344
+ </tr>
345
+ <% } %>
346
+ </tbody>
347
+ </table>
348
+
349
+ </div>
350
+
351
+ </body>
352
+ </html>
353
+ HTML
354
+
355
+
356
+ end
357
+ end
@@ -22,12 +22,25 @@ module Cramp
22
22
  # Overrides wrapper methods to run callbacks in a fiber
23
23
 
24
24
  def callback_wrapper
25
- self.fiber_pool.spawn { yield }
25
+ self.fiber_pool.spawn do
26
+ begin
27
+ yield
28
+ rescue StandardError, LoadError, SyntaxError => exception
29
+ handle_exception(exception)
30
+ end
31
+ end
26
32
  end
27
33
 
28
34
  def timer_method_wrapper(method)
29
- self.fiber_pool.spawn { send(method) }
35
+ self.fiber_pool.spawn do
36
+ begin
37
+ send(method)
38
+ rescue StandardError, LoadError, SyntaxError => exception
39
+ handle_exception(exception)
40
+ end
41
+ end
30
42
  end
43
+
31
44
  end
32
45
 
33
46
  end
@@ -47,6 +47,8 @@ module Cramp
47
47
 
48
48
  def timer_method_wrapper(method)
49
49
  send(method)
50
+ rescue StandardError, LoadError, SyntaxError => exception
51
+ handle_exception(exception)
50
52
  end
51
53
 
52
54
  end
@@ -1,9 +1,16 @@
1
+ require 'base64'
2
+ require 'digest/sha1'
3
+
1
4
  module Cramp
2
5
  module WebsocketExtension
3
6
  WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
4
7
 
8
+ def protocol_class
9
+ @env['HTTP_SEC_WEBSOCKET_VERSION'] ? Protocol10 : Protocol76
10
+ end
11
+
5
12
  def websocket?
6
- @env['HTTP_CONNECTION'] == 'Upgrade' && @env['HTTP_UPGRADE'] == 'WebSocket'
13
+ @env['HTTP_CONNECTION'] == 'Upgrade' && ['WebSocket', 'websocket'].include?(@env['HTTP_UPGRADE'])
7
14
  end
8
15
 
9
16
  def secure_websocket?
@@ -20,20 +27,23 @@ module Cramp
20
27
  end
21
28
 
22
29
  class WebSocketHandler
23
- def initialize(env, websocket_url, body)
30
+ def initialize(env, websocket_url, body = nil)
24
31
  @env = env
25
32
  @websocket_url = websocket_url
26
33
  @body = body
27
34
  end
28
35
  end
29
36
 
30
- class Protocol75 < WebSocketHandler
37
+ class Protocol10 < WebSocketHandler
38
+ MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".freeze
39
+
31
40
  def handshake
32
- upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
33
- upgrade << "Upgrade: WebSocket\r\n"
41
+ digest = Base64.encode64(Digest::SHA1.digest("#{@env['HTTP_SEC_WEBSOCKET_KEY']}#{MAGIC_GUID}")).chomp
42
+
43
+ upgrade = "HTTP/1.1 101 Switching Protocols\r\n"
44
+ upgrade << "Upgrade: websocket\r\n"
34
45
  upgrade << "Connection: Upgrade\r\n"
35
- upgrade << "WebSocket-Origin: #{@env['HTTP_ORIGIN']}\r\n"
36
- upgrade << "WebSocket-Location: #{@websocket_url}\r\n\r\n"
46
+ upgrade << "Sec-WebSocket-Accept: #{digest}\r\n\r\n"
37
47
  upgrade
38
48
  end
39
49
  end
@@ -0,0 +1,241 @@
1
+ # encoding: BINARY
2
+
3
+ # The MIT License - Copyright (c) 2009 Ilya Grigorik
4
+ # Thank you https://github.com/igrigorik/em-websocket
5
+ #
6
+ # Copyright (c) 2009 Ilya Grigorik
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to
13
+ # permit persons to whom the Software is furnished to do so, subject to
14
+ # the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+
27
+ module Cramp
28
+ class Protocol10FrameParser
29
+ class WebSocketError < RuntimeError; end
30
+
31
+ class MaskedString < String
32
+ # Read a 4 bit XOR mask - further requested bytes will be unmasked
33
+ def read_mask
34
+ if respond_to?(:encoding) && encoding.name != "ASCII-8BIT"
35
+ raise "MaskedString only operates on BINARY strings"
36
+ end
37
+ raise "Too short" if bytesize < 4 # TODO - change
38
+ @masking_key = String.new(self[0..3])
39
+ end
40
+
41
+ # Removes the mask, behaves like a normal string again
42
+ def unset_mask
43
+ @masking_key = nil
44
+ end
45
+
46
+ def slice_mask
47
+ slice!(0, 4)
48
+ end
49
+
50
+ def getbyte(index)
51
+ if @masking_key
52
+ masked_char = super
53
+ masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ def getbytes(start_index, count)
60
+ data = ''
61
+ count.times do |i|
62
+ data << getbyte(start_index + i)
63
+ end
64
+ data
65
+ end
66
+ end
67
+
68
+ attr_accessor :data
69
+
70
+ def initialize
71
+ @data = MaskedString.new
72
+ @application_data_buffer = '' # Used for MORE frames
73
+ end
74
+
75
+ def process_data
76
+ messages = []
77
+ error = false
78
+
79
+ while !error && @data.size >= 2
80
+ pointer = 0
81
+
82
+ fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
83
+ # Ignoring rsv1-3 for now
84
+ opcode = @data.getbyte(pointer) & 0b00001111
85
+ pointer += 1
86
+
87
+ mask = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
88
+ length = @data.getbyte(pointer) & 0b01111111
89
+ pointer += 1
90
+
91
+ raise WebSocketError, 'Data from client must be masked' unless mask
92
+
93
+ payload_length = case length
94
+ when 127 # Length defined by 8 bytes
95
+ # Check buffer size
96
+ if @data.getbyte(pointer+8-1) == nil
97
+ debug [:buffer_incomplete, @data]
98
+ error = true
99
+ next
100
+ end
101
+
102
+ # Only using the last 4 bytes for now, till I work out how to
103
+ # unpack 8 bytes. I'm sure 4GB frames will do for now :)
104
+ l = @data.getbytes(pointer+4, 4).unpack('N').first
105
+ pointer += 8
106
+ l
107
+ when 126 # Length defined by 2 bytes
108
+ # Check buffer size
109
+ if @data.getbyte(pointer+2-1) == nil
110
+ debug [:buffer_incomplete, @data]
111
+ error = true
112
+ next
113
+ end
114
+
115
+ l = @data.getbytes(pointer, 2).unpack('n').first
116
+ pointer += 2
117
+ l
118
+ else
119
+ length
120
+ end
121
+
122
+ # Compute the expected frame length
123
+ frame_length = pointer + payload_length
124
+ frame_length += 4 if mask
125
+
126
+ # Check buffer size
127
+ if @data.getbyte(frame_length - 1) == nil
128
+ debug [:buffer_incomplete, @data]
129
+ error = true
130
+ next
131
+ end
132
+
133
+ # Remove frame header
134
+ @data.slice!(0...pointer)
135
+ pointer = 0
136
+
137
+ # Read application data (unmasked if required)
138
+ @data.read_mask if mask
139
+ pointer += 4 if mask
140
+ application_data = @data.getbytes(pointer, payload_length)
141
+ pointer += payload_length
142
+ @data.unset_mask if mask
143
+
144
+ # Throw away data up to pointer
145
+ @data.slice!(0...pointer)
146
+
147
+ frame_type = opcode_to_type(opcode)
148
+
149
+ if frame_type == :continuation && !@frame_type
150
+ raise WebSocketError, 'Continuation frame not expected'
151
+ end
152
+
153
+ if !fin
154
+ debug [:moreframe, frame_type, application_data]
155
+ @application_data_buffer << application_data
156
+ @frame_type = frame_type
157
+ else
158
+ # Message is complete
159
+ if frame_type == :continuation
160
+ @application_data_buffer << application_data
161
+ messages << [@frame_type, @application_data_buffer]
162
+ @application_data_buffer = ''
163
+ @frame_type = nil
164
+ else
165
+ messages << [frame_type, application_data]
166
+ end
167
+ end
168
+ end # end while
169
+
170
+ messages
171
+ end
172
+
173
+ def send_frame(frame_type, application_data)
174
+ debug [:sending_frame, frame_type, application_data]
175
+
176
+ # Protocol10FrameParser doesn't have any knowledge of :closing in Cramp
177
+ # if @state == :closing && data_frame?(frame_type)
178
+ # raise WebSocketError, "Cannot send data frame since connection is closing"
179
+ # end
180
+
181
+ frame = ''
182
+
183
+ opcode = type_to_opcode(frame_type)
184
+ byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
185
+ frame << byte1
186
+
187
+ length = application_data.size
188
+ if length <= 125
189
+ byte2 = length # since rsv4 is 0
190
+ frame << byte2
191
+ elsif length < 65536 # write 2 byte length
192
+ frame << 126
193
+ frame << [length].pack('n')
194
+ else # write 8 byte length
195
+ frame << 127
196
+ frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
197
+ end
198
+
199
+ frame << application_data
200
+ end
201
+
202
+ def send_text_frame(data)
203
+ send_frame(:text, data)
204
+ end
205
+
206
+ private
207
+
208
+ FRAME_TYPES = {
209
+ :continuation => 0,
210
+ :text => 1,
211
+ :binary => 2,
212
+ :close => 8,
213
+ :ping => 9,
214
+ :pong => 10,
215
+ }
216
+ FRAME_TYPES_INVERSE = FRAME_TYPES.invert
217
+ # Frames are either data frames or control frames
218
+ DATA_FRAMES = [:text, :binary, :continuation]
219
+
220
+ def type_to_opcode(frame_type)
221
+ FRAME_TYPES[frame_type] || raise("Unknown frame type")
222
+ end
223
+
224
+ def opcode_to_type(opcode)
225
+ FRAME_TYPES_INVERSE[opcode] || raise(DataError, "Unknown opcode")
226
+ end
227
+
228
+ def data_frame?(type)
229
+ DATA_FRAMES.include?(type)
230
+ end
231
+
232
+ def debug(*data)
233
+ if @debug
234
+ require 'pp'
235
+ pp data
236
+ puts
237
+ end
238
+ end
239
+
240
+ end
241
+ end
@@ -24,9 +24,7 @@ class Cramp::Websocket::Rainbows < Rainbows::EventMachine::Client
24
24
  @state = :websocket
25
25
  @input.rewind
26
26
 
27
- handler = @env['HTTP_SEC_WEBSOCKET_KEY1'] &&
28
- @env['HTTP_SEC_WEBSOCKET_KEY2'] ? Protocol76 : Protocol75
29
- write(handler.new(@env, websocket_url, @buf).handshake)
27
+ write(protocol_class.new(@env, websocket_url, @buf).handshake)
30
28
  app_call NULL_IO
31
29
  else
32
30
  super
@@ -33,13 +33,7 @@ class Thin::Request
33
33
  include Cramp::WebsocketExtension
34
34
 
35
35
  def websocket_upgrade_data
36
- handler = if @env['HTTP_SEC_WEBSOCKET_KEY1'] and @env['HTTP_SEC_WEBSOCKET_KEY2']
37
- Protocol76
38
- else
39
- Protocol75
40
- end
41
-
42
- handler.new(@env, websocket_url, body.read).handshake
36
+ protocol_class.new(@env, websocket_url, body.read).handshake
43
37
  end
44
38
 
45
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cramp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: '0.15'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,12 +9,12 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-08-10 00:00:00.000000000 +01:00
12
+ date: 2011-08-13 00:00:00.000000000 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
17
- requirement: &2160171700 !ruby/object:Gem::Requirement
17
+ requirement: &2156847620 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ~>
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: 3.0.9
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2160171700
25
+ version_requirements: *2156847620
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rack
28
- requirement: &2160171100 !ruby/object:Gem::Requirement
28
+ requirement: &2156847100 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ~>
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: 1.3.2
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *2160171100
36
+ version_requirements: *2156847100
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: eventmachine
39
- requirement: &2160170520 !ruby/object:Gem::Requirement
39
+ requirement: &2156846640 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ~>
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: 1.0.0.beta.3
45
45
  type: :runtime
46
46
  prerelease: false
47
- version_requirements: *2160170520
47
+ version_requirements: *2156846640
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: thor
50
- requirement: &2160169960 !ruby/object:Gem::Requirement
50
+ requirement: &2156846160 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ~>
@@ -55,7 +55,7 @@ dependencies:
55
55
  version: 0.14.6
56
56
  type: :runtime
57
57
  prerelease: false
58
- version_requirements: *2160169960
58
+ version_requirements: *2156846160
59
59
  description: Cramp is a framework for developing asynchronous web applications.
60
60
  email: pratiknaik@gmail.com
61
61
  executables:
@@ -68,6 +68,7 @@ files:
68
68
  - lib/cramp/action.rb
69
69
  - lib/cramp/body.rb
70
70
  - lib/cramp/callbacks.rb
71
+ - lib/cramp/exception_handler.rb
71
72
  - lib/cramp/fiber_pool.rb
72
73
  - lib/cramp/generators/application.rb
73
74
  - lib/cramp/generators/templates/application/app/actions/home_action.rb
@@ -83,6 +84,7 @@ files:
83
84
  - lib/cramp/sse.rb
84
85
  - lib/cramp/test_case.rb
85
86
  - lib/cramp/websocket/extension.rb
87
+ - lib/cramp/websocket/protocol10_frame_parser.rb
86
88
  - lib/cramp/websocket/rainbows.rb
87
89
  - lib/cramp/websocket/rainbows_backend.rb
88
90
  - lib/cramp/websocket/thin_backend.rb
@@ -91,7 +93,7 @@ files:
91
93
  - lib/vendor/fiber_pool.rb
92
94
  - bin/cramp
93
95
  has_rdoc: false
94
- homepage: http://m.onkey.org
96
+ homepage: http://cramp.in
95
97
  licenses: []
96
98
  post_install_message:
97
99
  rdoc_options: []