cramp 0.14.1 → 0.15

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.
@@ -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: []