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.
- data/lib/cramp.rb +4 -1
- data/lib/cramp/abstract.rb +23 -8
- data/lib/cramp/action.rb +59 -8
- data/lib/cramp/callbacks.rb +44 -4
- data/lib/cramp/exception_handler.rb +357 -0
- data/lib/cramp/fiber_pool.rb +15 -2
- data/lib/cramp/periodic_timer.rb +2 -0
- data/lib/cramp/websocket/extension.rb +17 -7
- data/lib/cramp/websocket/protocol10_frame_parser.rb +241 -0
- data/lib/cramp/websocket/rainbows.rb +1 -3
- data/lib/cramp/websocket/thin_backend.rb +1 -7
- metadata +13 -11
data/lib/cramp.rb
CHANGED
@@ -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.
|
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
|
data/lib/cramp/abstract.rb
CHANGED
@@ -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
|
-
|
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
|
41
|
-
|
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
|
data/lib/cramp/action.rb
CHANGED
@@ -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
|
-
|
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/
|
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
|
-
|
36
|
-
headers['Content-Length'] = data.size.to_s
|
54
|
+
@_lp_headers['Content-Length'] = data.size.to_s
|
37
55
|
|
38
|
-
send_response(
|
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
|
-
|
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
|
data/lib/cramp/callbacks.rb
CHANGED
@@ -53,15 +53,55 @@ module Cramp
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def callback_wrapper
|
56
|
-
EM.next_tick
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
data/lib/cramp/fiber_pool.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/cramp/periodic_timer.rb
CHANGED
@@ -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']
|
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
|
37
|
+
class Protocol10 < WebSocketHandler
|
38
|
+
MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".freeze
|
39
|
+
|
31
40
|
def handshake
|
32
|
-
|
33
|
-
|
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-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
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: &
|
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: *
|
25
|
+
version_requirements: *2156847620
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rack
|
28
|
-
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: *
|
36
|
+
version_requirements: *2156847100
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: eventmachine
|
39
|
-
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: *
|
47
|
+
version_requirements: *2156846640
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: thor
|
50
|
-
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: *
|
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://
|
96
|
+
homepage: http://cramp.in
|
95
97
|
licenses: []
|
96
98
|
post_install_message:
|
97
99
|
rdoc_options: []
|