rack-contrib 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-contrib might be problematic. Click here for more details.
- data/AUTHORS +1 -0
- data/COPYING +1 -0
- data/README.rdoc +18 -1
- data/lib/rack/contrib.rb +4 -1
- data/lib/rack/contrib/accept_format.rb +1 -1
- data/lib/rack/contrib/common_cookies.rb +2 -4
- data/lib/rack/contrib/jsonp.rb +51 -5
- data/lib/rack/contrib/locale.rb +27 -16
- data/lib/rack/contrib/mailexceptions.rb +15 -33
- data/lib/rack/contrib/post_body_content_type_parser.rb +3 -3
- data/lib/rack/contrib/printout.rb +25 -0
- data/lib/rack/contrib/profiler.rb +21 -31
- data/lib/rack/contrib/static_cache.rb +2 -2
- data/lib/rack/contrib/try_static.rb +2 -2
- data/rack-contrib.gemspec +3 -2
- data/test/spec_rack_jsonp.rb +108 -1
- data/test/spec_rack_mailexceptions.rb +10 -12
- data/test/spec_rack_post_body_content_type_parser.rb +21 -13
- data/test/spec_rack_profiler.rb +8 -3
- data/test/spec_rack_static_cache.rb +13 -0
- data/test/spec_rack_try_static.rb +19 -7
- metadata +76 -90
data/AUTHORS
CHANGED
data/COPYING
CHANGED
data/README.rdoc
CHANGED
@@ -48,6 +48,7 @@ interface:
|
|
48
48
|
* Rack::ResponseHeaders - Manipulates response headers object at runtime
|
49
49
|
* Rack::SimpleEndpoint - Creates simple endpoints with routing rules, similar to Sinatra actions
|
50
50
|
* Rack::TryStatic - Tries to match request to a static file
|
51
|
+
* Rack::Printout - Prints the environment and the response per request
|
51
52
|
|
52
53
|
=== Use
|
53
54
|
|
@@ -73,9 +74,25 @@ components included. The following example shows what a simple rackup
|
|
73
74
|
|
74
75
|
run theapp
|
75
76
|
|
77
|
+
=== Testing
|
78
|
+
|
79
|
+
To contribute to the project, begin by cloning the repo and installing the necessary gems:
|
80
|
+
|
81
|
+
gem install json rack ruby-prof test-spec test-unit
|
82
|
+
|
83
|
+
To run the entire test suite, run
|
84
|
+
rake test
|
85
|
+
|
86
|
+
To run a specific component's tests run
|
87
|
+
specrb -Ilib:test -w test/spec_rack_thecomponent.rb
|
88
|
+
|
89
|
+
This works on ruby 1.8.7 but has problems under ruby 1.9.x.
|
90
|
+
|
91
|
+
TODO: instructions for 1.9.x and include bundler
|
92
|
+
|
76
93
|
=== Links
|
77
94
|
|
78
95
|
rack-contrib on GitHub:: <http://github.com/rack/rack-contrib>
|
79
96
|
Rack:: <http://rack.rubyforge.org/>
|
80
|
-
Rack On GitHub:: <http://github.
|
97
|
+
Rack On GitHub:: <http://github.com/rack/rack>
|
81
98
|
rack-devel mailing list:: <http://groups.google.com/group/rack-devel>
|
data/lib/rack/contrib.rb
CHANGED
@@ -3,7 +3,7 @@ require 'rack'
|
|
3
3
|
module Rack
|
4
4
|
module Contrib
|
5
5
|
def self.release
|
6
|
-
"1.0
|
6
|
+
"1.2.0"
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
@@ -14,6 +14,7 @@ module Rack
|
|
14
14
|
autoload :CSSHTTPRequest, "rack/contrib/csshttprequest"
|
15
15
|
autoload :Deflect, "rack/contrib/deflect"
|
16
16
|
autoload :ExpectationCascade, "rack/contrib/expectation_cascade"
|
17
|
+
autoload :HostMeta, "rack/contrib/host_meta"
|
17
18
|
autoload :GarbageCollector, "rack/contrib/garbagecollector"
|
18
19
|
autoload :JSONP, "rack/contrib/jsonp"
|
19
20
|
autoload :LighttpdScriptNameFix, "rack/contrib/lighttpd_script_name_fix"
|
@@ -36,4 +37,6 @@ module Rack
|
|
36
37
|
autoload :ResponseCache, "rack/contrib/response_cache"
|
37
38
|
autoload :RelativeRedirect, "rack/contrib/relative_redirect"
|
38
39
|
autoload :StaticCache, "rack/contrib/static_cache"
|
40
|
+
autoload :TryStatic, "rack/contrib/try_static"
|
41
|
+
autoload :Printout, "rack/contrib/printout"
|
39
42
|
end
|
@@ -37,7 +37,7 @@ module Rack
|
|
37
37
|
if ::File.extname(req.path_info).empty?
|
38
38
|
accept = env['HTTP_ACCEPT'].to_s.scan(/[^;,\s]*\/[^;,\s]*/)[0].to_s
|
39
39
|
extension = Rack::Mime::MIME_TYPES.invert[accept] || @ext
|
40
|
-
req.path_info = req.path_info
|
40
|
+
req.path_info = req.path_info.chomp('/') << "#{extension}"
|
41
41
|
end
|
42
42
|
|
43
43
|
@app.call(env)
|
data/lib/rack/contrib/jsonp.rb
CHANGED
@@ -7,6 +7,21 @@ module Rack
|
|
7
7
|
class JSONP
|
8
8
|
include Rack::Utils
|
9
9
|
|
10
|
+
VALID_JS_VAR = /[a-zA-Z_$][\w$]*/
|
11
|
+
VALID_CALLBACK = /\A#{VALID_JS_VAR}(?:\.?#{VALID_JS_VAR})*\z/
|
12
|
+
|
13
|
+
# These hold the Unicode characters \u2028 and \u2029.
|
14
|
+
#
|
15
|
+
# They are defined in constants for Ruby 1.8 compatibility.
|
16
|
+
#
|
17
|
+
# In 1.8
|
18
|
+
# "\u2028" # => "u2028"
|
19
|
+
# "\u2029" # => "u2029"
|
20
|
+
# In 1.9
|
21
|
+
# "\342\200\250" # => "\u2028"
|
22
|
+
# "\342\200\251" # => "\u2029"
|
23
|
+
U2028, U2029 = ("\u2028" == 'u2028') ? ["\342\200\250", "\342\200\251"] : ["\u2028", "\u2029"]
|
24
|
+
|
10
25
|
def initialize(app)
|
11
26
|
@app = app
|
12
27
|
end
|
@@ -18,13 +33,17 @@ module Rack
|
|
18
33
|
# Changes nothing if no <tt>callback</tt> param is specified.
|
19
34
|
#
|
20
35
|
def call(env)
|
36
|
+
request = Rack::Request.new(env)
|
37
|
+
|
21
38
|
status, headers, response = @app.call(env)
|
22
39
|
|
23
40
|
headers = HeaderHash.new(headers)
|
24
|
-
request = Rack::Request.new(env)
|
25
41
|
|
26
42
|
if is_json?(headers) && has_callback?(request)
|
27
|
-
|
43
|
+
callback = request.params['callback']
|
44
|
+
return bad_request unless valid_callback?(callback)
|
45
|
+
|
46
|
+
response = pad(callback, response)
|
28
47
|
|
29
48
|
# No longer json, its javascript!
|
30
49
|
headers['Content-Type'] = headers['Content-Type'].gsub('json', 'javascript')
|
@@ -35,6 +54,7 @@ module Rack
|
|
35
54
|
headers['Content-Length'] = length.to_s
|
36
55
|
end
|
37
56
|
end
|
57
|
+
|
38
58
|
[status, headers, response]
|
39
59
|
end
|
40
60
|
|
@@ -45,7 +65,16 @@ module Rack
|
|
45
65
|
end
|
46
66
|
|
47
67
|
def has_callback?(request)
|
48
|
-
request.params.include?('callback')
|
68
|
+
request.params.include?('callback') and not request.params['callback'].empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
# See:
|
72
|
+
# http://stackoverflow.com/questions/1661197/valid-characters-for-javascript-variable-names
|
73
|
+
#
|
74
|
+
# NOTE: Supports dots (.) since callbacks are often in objects:
|
75
|
+
#
|
76
|
+
def valid_callback?(callback)
|
77
|
+
callback =~ VALID_CALLBACK
|
49
78
|
end
|
50
79
|
|
51
80
|
# Pads the response with the appropriate callback format according to the
|
@@ -56,8 +85,25 @@ module Rack
|
|
56
85
|
# since JSON is returned as a full string.
|
57
86
|
#
|
58
87
|
def pad(callback, response, body = "")
|
59
|
-
response.each
|
60
|
-
|
88
|
+
response.each do |s|
|
89
|
+
# U+2028 and U+2029 are allowed inside strings in JSON (as all literal
|
90
|
+
# Unicode characters) but JavaScript defines them as newline
|
91
|
+
# seperators. Because no literal newlines are allowed in a string, this
|
92
|
+
# causes a ParseError in the browser. We work around this issue by
|
93
|
+
# replacing them with the escaped version. This should be safe because
|
94
|
+
# according to the JSON spec, these characters are *only* valid inside
|
95
|
+
# a string and should therefore not be present any other places.
|
96
|
+
body << s.to_s.gsub(U2028, '\u2028').gsub(U2029, '\u2029')
|
97
|
+
end
|
98
|
+
|
99
|
+
# https://github.com/rack/rack-contrib/issues/46
|
100
|
+
response.close if response.respond_to?(:close)
|
101
|
+
|
102
|
+
["/**/#{callback}(#{body})"]
|
103
|
+
end
|
104
|
+
|
105
|
+
def bad_request(body = "Bad Request")
|
106
|
+
[ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s }, [body] ]
|
61
107
|
end
|
62
108
|
|
63
109
|
end
|
data/lib/rack/contrib/locale.rb
CHANGED
@@ -8,24 +8,35 @@ module Rack
|
|
8
8
|
|
9
9
|
def call(env)
|
10
10
|
old_locale = I18n.locale
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
locale =
|
20
|
-
else
|
21
|
-
locale = I18n.default_locale
|
11
|
+
|
12
|
+
begin
|
13
|
+
locale = accept_locale(env) || I18n.default_locale
|
14
|
+
locale = env['rack.locale'] = I18n.locale = locale.to_s
|
15
|
+
status, headers, body = @app.call(env)
|
16
|
+
headers['Content-Language'] = locale
|
17
|
+
[status, headers, body]
|
18
|
+
ensure
|
19
|
+
I18n.locale = old_locale
|
22
20
|
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
26
|
+
def accept_locale(env)
|
27
|
+
accept_langs = env["HTTP_ACCEPT_LANGUAGE"]
|
28
|
+
return if accept_langs.nil?
|
29
|
+
|
30
|
+
languages_and_qvalues = accept_langs.split(",").map { |l|
|
31
|
+
l += ';q=1.0' unless l =~ /;q=\d+(?:\.\d+)?$/
|
32
|
+
l.split(';q=')
|
33
|
+
}
|
34
|
+
|
35
|
+
lang = languages_and_qvalues.sort_by { |(locale, qvalue)|
|
36
|
+
qvalue.to_f
|
37
|
+
}.last.first
|
23
38
|
|
24
|
-
|
25
|
-
status, headers, body = @app.call(env)
|
26
|
-
headers['Content-Language'] = locale
|
27
|
-
I18n.locale = old_locale
|
28
|
-
[status, headers, body]
|
39
|
+
lang == '*' ? nil : lang
|
29
40
|
end
|
30
41
|
end
|
31
42
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'net/smtp'
|
2
|
-
require '
|
2
|
+
require 'mail'
|
3
3
|
require 'erb'
|
4
4
|
|
5
5
|
module Rack
|
@@ -14,10 +14,10 @@ module Rack
|
|
14
14
|
@app = app
|
15
15
|
@config = {
|
16
16
|
:to => nil,
|
17
|
-
:from => ENV['USER'] || 'rack',
|
17
|
+
:from => ENV['USER'] || 'rack@localhost',
|
18
18
|
:subject => '[exception] %s',
|
19
19
|
:smtp => {
|
20
|
-
:
|
20
|
+
:address => 'localhost',
|
21
21
|
:domain => 'localhost',
|
22
22
|
:port => 25,
|
23
23
|
:authentication => :login,
|
@@ -34,8 +34,6 @@ module Rack
|
|
34
34
|
begin
|
35
35
|
@app.call(env)
|
36
36
|
rescue => boom
|
37
|
-
# TODO don't allow exceptions from send_notification to
|
38
|
-
# propogate
|
39
37
|
send_notification boom, env
|
40
38
|
raise
|
41
39
|
end
|
@@ -53,40 +51,24 @@ module Rack
|
|
53
51
|
|
54
52
|
private
|
55
53
|
def generate_mail(exception, env)
|
56
|
-
mail =
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
mail.charset = 'UTF-8'
|
63
|
-
mail.body = @template.result(binding)
|
64
|
-
mail
|
54
|
+
mail = Mail.new({
|
55
|
+
:from => config[:from],
|
56
|
+
:to => config[:to],
|
57
|
+
:subject => config[:subject] % [exception.to_s],
|
58
|
+
:body => @template.result(binding)
|
59
|
+
})
|
65
60
|
end
|
66
61
|
|
67
62
|
def send_notification(exception, env)
|
68
63
|
mail = generate_mail(exception, env)
|
69
64
|
smtp = config[:smtp]
|
65
|
+
# for backward compability, replace the :server key with :address
|
66
|
+
address = smtp.delete :server
|
67
|
+
smtp[:address] = address if address
|
68
|
+
mail.delivery_method :smtp, smtp
|
69
|
+
mail.deliver!
|
70
70
|
env['mail.sent'] = true
|
71
|
-
|
72
|
-
|
73
|
-
server = service.new(smtp[:server], smtp[:port])
|
74
|
-
|
75
|
-
if smtp[:enable_starttls_auto] == :auto
|
76
|
-
server.enable_starttls_auto
|
77
|
-
elsif smtp[:enable_starttls_auto]
|
78
|
-
server.enable_starttls
|
79
|
-
end
|
80
|
-
|
81
|
-
server.start smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication]
|
82
|
-
|
83
|
-
mail.to.each do |recipient|
|
84
|
-
server.send_message mail.to_s, mail.from, recipient
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def service
|
89
|
-
Net::SMTP
|
71
|
+
mail
|
90
72
|
end
|
91
73
|
|
92
74
|
def extract_body(env)
|
@@ -29,9 +29,9 @@ module Rack
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def call(env)
|
32
|
-
|
33
|
-
|
34
|
-
env.update(FORM_HASH => JSON.parse(
|
32
|
+
if Rack::Request.new(env).media_type == APPLICATION_JSON && (body = env[POST_BODY].read).length != 0
|
33
|
+
env[POST_BODY].rewind # somebody might try to read this stream
|
34
|
+
env.update(FORM_HASH => JSON.parse(body), FORM_INPUT => env[POST_BODY])
|
35
35
|
end
|
36
36
|
@app.call(env)
|
37
37
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rack
|
2
|
+
#prints the environment and request for simple debugging
|
3
|
+
class Printout
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
# See http://rack.rubyforge.org/doc/SPEC.html for details
|
10
|
+
puts "**********\n Environment\n **************"
|
11
|
+
puts env.inspect
|
12
|
+
|
13
|
+
puts "**********\n Response\n **************"
|
14
|
+
response = @app.call(env)
|
15
|
+
puts response.inspect
|
16
|
+
|
17
|
+
puts "**********\n Response contents\n **************"
|
18
|
+
response[2].each do |chunk|
|
19
|
+
puts chunk
|
20
|
+
end
|
21
|
+
puts "\n \n"
|
22
|
+
return response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -6,30 +6,22 @@ module Rack
|
|
6
6
|
#
|
7
7
|
# Pass the :printer option to pick a different result format.
|
8
8
|
class Profiler
|
9
|
-
MODES = %w(
|
10
|
-
|
11
|
-
wall_time
|
12
|
-
cpu_time
|
13
|
-
allocations
|
14
|
-
memory
|
15
|
-
gc_runs
|
16
|
-
gc_time
|
17
|
-
)
|
9
|
+
MODES = %w(process_time wall_time cpu_time
|
10
|
+
allocations memory gc_runs gc_time)
|
18
11
|
|
19
|
-
DEFAULT_PRINTER =
|
20
|
-
DEFAULT_CONTENT_TYPE = 'application/octet-stream'
|
12
|
+
DEFAULT_PRINTER = :call_stack
|
21
13
|
|
22
|
-
|
23
|
-
RubyProf::FlatPrinter
|
24
|
-
RubyProf::GraphPrinter
|
25
|
-
RubyProf::GraphHtmlPrinter => 'text/html'
|
26
|
-
|
14
|
+
CONTENT_TYPES = Hash.new('application/octet-stream').merge(
|
15
|
+
'RubyProf::FlatPrinter' => 'text/plain',
|
16
|
+
'RubyProf::GraphPrinter' => 'text/plain',
|
17
|
+
'RubyProf::GraphHtmlPrinter' => 'text/html',
|
18
|
+
'RubyProf::CallStackPrinter' => 'text/html')
|
27
19
|
|
28
|
-
# Accepts a :printer => [:call_tree|:graph_html|:graph|:flat]
|
29
|
-
# defaulting to :
|
20
|
+
# Accepts a :printer => [:call_stack|:call_tree|:graph_html|:graph|:flat]
|
21
|
+
# option defaulting to :call_stack.
|
30
22
|
def initialize(app, options = {})
|
31
23
|
@app = app
|
32
|
-
@printer = parse_printer(options[:printer])
|
24
|
+
@printer = parse_printer(options[:printer] || DEFAULT_PRINTER)
|
33
25
|
@times = (options[:times] || 1).to_i
|
34
26
|
end
|
35
27
|
|
@@ -43,10 +35,10 @@ module Rack
|
|
43
35
|
|
44
36
|
private
|
45
37
|
def profiling?(env)
|
46
|
-
unless RubyProf.running?
|
38
|
+
unless ::RubyProf.running?
|
47
39
|
request = Rack::Request.new(env.clone)
|
48
40
|
if mode = request.params.delete('profile')
|
49
|
-
if RubyProf.const_defined?(mode.upcase)
|
41
|
+
if ::RubyProf.const_defined?(mode.upcase)
|
50
42
|
mode
|
51
43
|
else
|
52
44
|
env['rack.errors'].write "Invalid RubyProf measure_mode: " +
|
@@ -58,10 +50,10 @@ module Rack
|
|
58
50
|
end
|
59
51
|
|
60
52
|
def profile(env, mode)
|
61
|
-
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
|
53
|
+
::RubyProf.measure_mode = ::RubyProf.const_get(mode.upcase)
|
62
54
|
|
63
55
|
GC.enable_stats if GC.respond_to?(:enable_stats)
|
64
|
-
result = RubyProf.profile do
|
56
|
+
result = ::RubyProf.profile do
|
65
57
|
@times.times { @app.call(env) }
|
66
58
|
end
|
67
59
|
GC.disable_stats if GC.respond_to?(:disable_stats)
|
@@ -77,8 +69,8 @@ module Rack
|
|
77
69
|
end
|
78
70
|
|
79
71
|
def headers(printer, env, mode)
|
80
|
-
headers = { 'Content-Type' =>
|
81
|
-
if printer == RubyProf::CallTreePrinter
|
72
|
+
headers = { 'Content-Type' => CONTENT_TYPES[printer.name] }
|
73
|
+
if printer == ::RubyProf::CallTreePrinter
|
82
74
|
filename = ::File.basename(env['PATH_INFO'])
|
83
75
|
headers['Content-Disposition'] =
|
84
76
|
%(attachment; filename="#{filename}.#{mode}.tree")
|
@@ -87,16 +79,14 @@ module Rack
|
|
87
79
|
end
|
88
80
|
|
89
81
|
def parse_printer(printer)
|
90
|
-
if printer.
|
91
|
-
DEFAULT_PRINTER
|
92
|
-
elsif printer.is_a?(Class)
|
82
|
+
if printer.is_a?(Class)
|
93
83
|
printer
|
94
84
|
else
|
95
85
|
name = "#{camel_case(printer)}Printer"
|
96
|
-
if RubyProf.const_defined?(name)
|
97
|
-
RubyProf.const_get(name)
|
86
|
+
if ::RubyProf.const_defined?(name)
|
87
|
+
::RubyProf.const_get(name)
|
98
88
|
else
|
99
|
-
|
89
|
+
::RubyProf::FlatPrinter
|
100
90
|
end
|
101
91
|
end
|
102
92
|
end
|
@@ -26,7 +26,7 @@ module Rack
|
|
26
26
|
#
|
27
27
|
# Examples:
|
28
28
|
# use Rack::StaticCache, :urls => ["/images", "/css", "/js", "/documents*"], :root => "statics"
|
29
|
-
# will serve all requests beginning with /images, /
|
29
|
+
# will serve all requests beginning with /images, /css or /js from the
|
30
30
|
# directory "statics/images", "statics/css", "statics/js".
|
31
31
|
# All the files from these directories will have modified headers to enable client/proxy caching,
|
32
32
|
# except the files from the directory "documents". Append a * (star) at the end of the pattern
|
@@ -87,7 +87,7 @@ module Rack
|
|
87
87
|
end
|
88
88
|
|
89
89
|
def duration_in_seconds
|
90
|
-
60 * 60 * 24 * 365 * @cache_duration
|
90
|
+
(60 * 60 * 24 * 365 * @cache_duration).to_i
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
data/rack-contrib.gemspec
CHANGED
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
|
|
3
3
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
4
|
|
5
5
|
s.name = 'rack-contrib'
|
6
|
-
s.version = '1.
|
7
|
-
s.
|
6
|
+
s.version = '1.2.0'
|
7
|
+
s.licenses = ['MIT']
|
8
8
|
|
9
9
|
s.description = "Contributed Rack Middleware and Utilities"
|
10
10
|
s.summary = "Contributed Rack Middleware and Utilities"
|
@@ -41,6 +41,7 @@ Gem::Specification.new do |s|
|
|
41
41
|
lib/rack/contrib/not_found.rb
|
42
42
|
lib/rack/contrib/post_body_content_type_parser.rb
|
43
43
|
lib/rack/contrib/proctitle.rb
|
44
|
+
lib/rack/contrib/printout.rb
|
44
45
|
lib/rack/contrib/profiler.rb
|
45
46
|
lib/rack/contrib/relative_redirect.rb
|
46
47
|
lib/rack/contrib/response_cache.rb
|
data/test/spec_rack_jsonp.rb
CHANGED
@@ -51,6 +51,113 @@ context "Rack::JSONP" do
|
|
51
51
|
headers = Rack::JSONP.new(app).call(request)[1]
|
52
52
|
headers['Content-Type'].should.equal('application/javascript')
|
53
53
|
end
|
54
|
+
|
55
|
+
specify "should not allow literal U+2028 or U+2029" do
|
56
|
+
test_body = unless "\u2028" == 'u2028'
|
57
|
+
"{\"bar\":\"\u2028 and \u2029\"}"
|
58
|
+
else
|
59
|
+
"{\"bar\":\"\342\200\250 and \342\200\251\"}"
|
60
|
+
end
|
61
|
+
callback = 'foo'
|
62
|
+
app = lambda { |env| [200, {'Content-Type' => 'application/json'}, [test_body]] }
|
63
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
64
|
+
body = Rack::JSONP.new(app).call(request).last
|
65
|
+
unless "\u2028" == 'u2028'
|
66
|
+
body.join.should.not.match(/\u2028|\u2029/)
|
67
|
+
else
|
68
|
+
body.join.should.not.match(/\342\200\250|\342\200\251/)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "but is empty" do
|
73
|
+
specify "should " do
|
74
|
+
test_body = '{"bar":"foo"}'
|
75
|
+
callback = ''
|
76
|
+
app = lambda { |env| [200, {'Content-Type' => 'application/json'}, [test_body]] }
|
77
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
78
|
+
body = Rack::JSONP.new(app).call(request).last
|
79
|
+
body.should.equal ['{"bar":"foo"}']
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'but is invalid' do
|
84
|
+
context 'with content-type application/json' do
|
85
|
+
specify 'should return "Bad Request"' do
|
86
|
+
test_body = '{"bar":"foo"}'
|
87
|
+
callback = '*'
|
88
|
+
content_type = 'application/json'
|
89
|
+
app = lambda { |env| [200, {'Content-Type' => content_type}, [test_body]] }
|
90
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
91
|
+
body = Rack::JSONP.new(app).call(request).last
|
92
|
+
body.should.equal ['Bad Request']
|
93
|
+
end
|
94
|
+
|
95
|
+
specify 'should return set the response code to 400' do
|
96
|
+
test_body = '{"bar":"foo"}'
|
97
|
+
callback = '*'
|
98
|
+
content_type = 'application/json'
|
99
|
+
app = lambda { |env| [200, {'Content-Type' => content_type}, [test_body]] }
|
100
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
101
|
+
response_code = Rack::JSONP.new(app).call(request).first
|
102
|
+
response_code.should.equal 400
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'with content-type text/plain' do
|
107
|
+
specify 'should return "Good Request"' do
|
108
|
+
test_body = 'Good Request'
|
109
|
+
callback = '*'
|
110
|
+
content_type = 'text/plain'
|
111
|
+
app = lambda { |env| [200, {'Content-Type' => content_type}, [test_body]] }
|
112
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
113
|
+
body = Rack::JSONP.new(app).call(request).last
|
114
|
+
body.should.equal ['Good Request']
|
115
|
+
end
|
116
|
+
|
117
|
+
specify 'should not change the response code from 200' do
|
118
|
+
test_body = '{"bar":"foo"}'
|
119
|
+
callback = '*'
|
120
|
+
content_type = 'text/plain'
|
121
|
+
app = lambda { |env| [200, {'Content-Type' => content_type}, [test_body]] }
|
122
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
123
|
+
response_code = Rack::JSONP.new(app).call(request).first
|
124
|
+
response_code.should.equal 200
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "with XSS vulnerability attempts" do
|
130
|
+
def request(callback, body = '{"bar":"foo"}')
|
131
|
+
app = lambda { |env| [200, {'Content-Type' => 'application/json'}, [body]] }
|
132
|
+
request = Rack::MockRequest.env_for("/", :params => "foo=bar&callback=#{callback}")
|
133
|
+
Rack::JSONP.new(app).call(request)
|
134
|
+
end
|
135
|
+
|
136
|
+
def assert_bad_request(response)
|
137
|
+
response.should.not.equal nil
|
138
|
+
status, headers, body = response
|
139
|
+
status.should.equal 400
|
140
|
+
body.should.equal ["Bad Request"]
|
141
|
+
end
|
142
|
+
|
143
|
+
specify "should return bad request for callback with invalid characters" do
|
144
|
+
assert_bad_request(request("foo<bar>baz()$"))
|
145
|
+
end
|
146
|
+
|
147
|
+
specify "should return bad request for callbacks with <script> tags" do
|
148
|
+
assert_bad_request(request("foo<script>alert(1)</script>"))
|
149
|
+
end
|
150
|
+
|
151
|
+
specify "should return bad requests for callbacks with multiple statements" do
|
152
|
+
assert_bad_request(request("foo%3balert(1)//")) # would render: "foo;alert(1)//"
|
153
|
+
end
|
154
|
+
|
155
|
+
specify "should not return a bad request for callbacks with dots in the callback" do
|
156
|
+
status, headers, body = request(callback = "foo.bar.baz", test_body = '{"foo":"bar"}')
|
157
|
+
status.should.equal 200
|
158
|
+
body.should.equal ["#{callback}(#{test_body})"]
|
159
|
+
end
|
160
|
+
end
|
54
161
|
|
55
162
|
end
|
56
163
|
|
@@ -78,4 +185,4 @@ context "Rack::JSONP" do
|
|
78
185
|
body.should.equal [test_body]
|
79
186
|
end
|
80
187
|
|
81
|
-
end
|
188
|
+
end
|
@@ -2,10 +2,8 @@ require 'test/spec'
|
|
2
2
|
require 'rack/mock'
|
3
3
|
|
4
4
|
begin
|
5
|
-
require '
|
6
|
-
require '
|
7
|
-
|
8
|
-
require File.dirname(__FILE__) + '/mail_settings.rb'
|
5
|
+
require './lib/rack/contrib/mailexceptions'
|
6
|
+
require './test/mail_settings.rb'
|
9
7
|
|
10
8
|
class TestError < RuntimeError
|
11
9
|
end
|
@@ -57,13 +55,13 @@ begin
|
|
57
55
|
mail.smtp @smtp_settings
|
58
56
|
end
|
59
57
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
58
|
+
mail = mailer.send(:generate_mail, test_exception, @env)
|
59
|
+
mail.to.should.equal ['foo@example.org']
|
60
|
+
mail.from.should.equal ['bar@example.org']
|
61
|
+
mail.subject.should.equal '[ERROR] Suffering Succotash!'
|
62
|
+
mail.body.should.not.be.nil
|
63
|
+
mail.body.should.be =~ /FOO:\s+"BAR"/
|
64
|
+
mail.body.should.be =~ /^\s*THE BODY\s*$/
|
67
65
|
end
|
68
66
|
|
69
67
|
specify 'catches exceptions raised from app, sends mail, and re-raises' do
|
@@ -167,5 +165,5 @@ begin
|
|
167
165
|
end
|
168
166
|
end
|
169
167
|
rescue LoadError => boom
|
170
|
-
STDERR.puts "WARN: Skipping Rack::MailExceptions tests (
|
168
|
+
STDERR.puts "WARN: Skipping Rack::MailExceptions tests (mail not installed)"
|
171
169
|
end
|
@@ -6,26 +6,34 @@ begin
|
|
6
6
|
|
7
7
|
context "Rack::PostBodyContentTypeParser" do
|
8
8
|
|
9
|
-
specify "should
|
10
|
-
|
11
|
-
|
12
|
-
body = Rack::PostBodyContentTypeParser.new(app).call(env).last
|
13
|
-
body['body'].should.equal "asdf"
|
14
|
-
body['status'].should.equal "12"
|
9
|
+
specify "should parse 'application/json' requests" do
|
10
|
+
params = params_for_request '{"key":"value"}', "application/json"
|
11
|
+
params['key'].should.equal "value"
|
15
12
|
end
|
16
13
|
|
17
|
-
specify "should
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
specify "should parse 'application/json; charset=utf-8' requests" do
|
15
|
+
params = params_for_request '{"key":"value"}', "application/json; charset=utf-8"
|
16
|
+
params['key'].should.equal "value"
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "should parse 'application/json' requests with empty body" do
|
20
|
+
params = params_for_request "", "application/json"
|
21
|
+
params.should.equal({})
|
22
|
+
end
|
23
|
+
|
24
|
+
specify "shouldn't affect form-urlencoded requests" do
|
25
|
+
params = params_for_request("key=value", "application/x-www-form-urlencoded")
|
26
|
+
params['key'].should.equal "value"
|
22
27
|
end
|
23
28
|
|
24
29
|
end
|
25
30
|
|
26
|
-
def
|
27
|
-
Rack::MockRequest.env_for
|
31
|
+
def params_for_request(body, content_type)
|
32
|
+
env = Rack::MockRequest.env_for "/", {:method => "POST", :input => body, "CONTENT_TYPE" => content_type}
|
33
|
+
app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, Rack::Request.new(env).POST] }
|
34
|
+
Rack::PostBodyContentTypeParser.new(app).call(env).last
|
28
35
|
end
|
36
|
+
|
29
37
|
rescue LoadError => e
|
30
38
|
# Missing dependency JSON, skipping tests.
|
31
39
|
STDERR.puts "WARN: Skipping Rack::PostBodyContentTypeParser tests (json not installed)"
|
data/test/spec_rack_profiler.rb
CHANGED
@@ -8,14 +8,19 @@ begin
|
|
8
8
|
app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'Oh hai der'] }
|
9
9
|
request = Rack::MockRequest.env_for("/", :params => "profile=process_time")
|
10
10
|
|
11
|
-
specify 'printer defaults to RubyProf::
|
11
|
+
specify 'printer defaults to RubyProf::CallStackPrinter' do
|
12
12
|
profiler = Rack::Profiler.new(nil)
|
13
|
-
profiler.instance_variable_get('@printer').should.equal RubyProf::
|
13
|
+
profiler.instance_variable_get('@printer').should.equal RubyProf::CallStackPrinter
|
14
14
|
profiler.instance_variable_get('@times').should.equal 1
|
15
15
|
end
|
16
16
|
|
17
|
+
specify 'CallStackPrinter has Content-Type test/html' do
|
18
|
+
headers = Rack::Profiler.new(app, :printer => :call_stack).call(request)[1]
|
19
|
+
headers.should.equal "Content-Type"=>"text/html"
|
20
|
+
end
|
21
|
+
|
17
22
|
specify 'CallTreePrinter has correct headers' do
|
18
|
-
headers = Rack::Profiler.new(app).call(request)[1]
|
23
|
+
headers = Rack::Profiler.new(app, :printer => :call_tree).call(request)[1]
|
19
24
|
headers.should.equal "Content-Disposition"=>"attachment; filename=\"/.process_time.tree\"", "Content-Type"=>"application/octet-stream"
|
20
25
|
end
|
21
26
|
|
@@ -59,6 +59,14 @@ describe "Rack::StaticCache" do
|
|
59
59
|
res.headers['Expires'].should =~ Regexp.new("#{next_next_year}")
|
60
60
|
end
|
61
61
|
|
62
|
+
it "should round max-age if duration is part of a year" do
|
63
|
+
one_week_duration_app_request
|
64
|
+
res = @request.get("/statics/test")
|
65
|
+
res.should.be.ok
|
66
|
+
res.body.should =~ /rubyrack/
|
67
|
+
res.headers['Cache-Control'].should == "max-age=606461, public"
|
68
|
+
end
|
69
|
+
|
62
70
|
it "should return 404s if requested with version number but versioning is disabled" do
|
63
71
|
configured_app_request
|
64
72
|
res = @request.get("/statics/test-0.0.1")
|
@@ -79,6 +87,11 @@ describe "Rack::StaticCache" do
|
|
79
87
|
request
|
80
88
|
end
|
81
89
|
|
90
|
+
def one_week_duration_app_request
|
91
|
+
@options = {:urls => ["/statics"], :root => @root, :duration => 1.fdiv(52)}
|
92
|
+
request
|
93
|
+
end
|
94
|
+
|
82
95
|
def configured_app_request
|
83
96
|
@options = {:urls => ["/statics", "/documents*"], :root => @root, :versioning => false, :duration => 2}
|
84
97
|
request
|
@@ -4,23 +4,25 @@ require 'rack'
|
|
4
4
|
require 'rack/contrib/try_static'
|
5
5
|
require 'rack/mock'
|
6
6
|
|
7
|
-
def
|
8
|
-
|
7
|
+
def build_options(opts)
|
8
|
+
{
|
9
9
|
:urls => %w[/],
|
10
10
|
:root => ::File.expand_path(::File.dirname(__FILE__)),
|
11
|
-
})
|
11
|
+
}.merge(opts)
|
12
|
+
end
|
12
13
|
|
14
|
+
def request(options = {})
|
13
15
|
@request =
|
14
16
|
Rack::MockRequest.new(
|
15
17
|
Rack::TryStatic.new(
|
16
|
-
lambda {[200, {}, ["Hello World"]]},
|
18
|
+
lambda { |_| [200, {}, ["Hello World"]]},
|
17
19
|
options))
|
18
20
|
end
|
19
21
|
|
20
22
|
describe "Rack::TryStatic" do
|
21
23
|
context 'when file cannot be found' do
|
22
24
|
it 'should call call app' do
|
23
|
-
res = request(:try => ['html']).get('/documents')
|
25
|
+
res = request(build_options(:try => ['html'])).get('/documents')
|
24
26
|
res.should.be.ok
|
25
27
|
res.body.should == "Hello World"
|
26
28
|
end
|
@@ -28,7 +30,7 @@ describe "Rack::TryStatic" do
|
|
28
30
|
|
29
31
|
context 'when file can be found' do
|
30
32
|
it 'should serve first found' do
|
31
|
-
res = request(:try => ['.html', '/index.html', '/index.htm']).get('/documents')
|
33
|
+
res = request(build_options(:try => ['.html', '/index.html', '/index.htm'])).get('/documents')
|
32
34
|
res.should.be.ok
|
33
35
|
res.body.strip.should == "index.html"
|
34
36
|
end
|
@@ -36,9 +38,19 @@ describe "Rack::TryStatic" do
|
|
36
38
|
|
37
39
|
context 'when path_info maps directly to file' do
|
38
40
|
it 'should serve existing' do
|
39
|
-
res = request(:try => ['/index.html']).get('/documents/existing.html')
|
41
|
+
res = request(build_options(:try => ['/index.html'])).get('/documents/existing.html')
|
40
42
|
res.should.be.ok
|
41
43
|
res.body.strip.should == "existing.html"
|
42
44
|
end
|
43
45
|
end
|
46
|
+
|
47
|
+
context 'when sharing options' do
|
48
|
+
it 'should not mutate given options' do
|
49
|
+
org_options = build_options :try => ['/index.html']
|
50
|
+
given_options = org_options.dup
|
51
|
+
request(given_options).get('/documents').should.be.ok
|
52
|
+
request(given_options).get('/documents').should.be.ok
|
53
|
+
given_options.should == org_options
|
54
|
+
end
|
55
|
+
end
|
44
56
|
end
|
metadata
CHANGED
@@ -1,95 +1,88 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-contrib
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 1
|
8
|
-
- 1
|
9
|
-
- 0
|
10
|
-
version: 1.1.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.0
|
5
|
+
prerelease:
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
7
|
+
authors:
|
13
8
|
- rack-devel
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
dependencies:
|
21
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2014-10-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
22
15
|
name: rack
|
23
|
-
|
24
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
25
17
|
none: false
|
26
|
-
requirements:
|
27
|
-
- -
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
hash: 57
|
30
|
-
segments:
|
31
|
-
- 0
|
32
|
-
- 9
|
33
|
-
- 1
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
34
21
|
version: 0.9.1
|
35
22
|
type: :runtime
|
36
|
-
version_requirements: *id001
|
37
|
-
- !ruby/object:Gem::Dependency
|
38
|
-
name: test-spec
|
39
23
|
prerelease: false
|
40
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.9.1
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: test-spec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
41
33
|
none: false
|
42
|
-
requirements:
|
43
|
-
- -
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
hash: 59
|
46
|
-
segments:
|
47
|
-
- 0
|
48
|
-
- 9
|
49
|
-
- 0
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
50
37
|
version: 0.9.0
|
51
38
|
type: :development
|
52
|
-
version_requirements: *id002
|
53
|
-
- !ruby/object:Gem::Dependency
|
54
|
-
name: tmail
|
55
39
|
prerelease: false
|
56
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
41
|
none: false
|
58
|
-
requirements:
|
59
|
-
- -
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.9.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: tmail
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.2'
|
66
54
|
type: :development
|
67
|
-
version_requirements: *id003
|
68
|
-
- !ruby/object:Gem::Dependency
|
69
|
-
name: json
|
70
55
|
prerelease: false
|
71
|
-
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
57
|
none: false
|
73
|
-
requirements:
|
74
|
-
- -
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.2'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: json
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.1'
|
81
70
|
type: :development
|
82
|
-
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.1'
|
83
78
|
description: Contributed Rack Middleware and Utilities
|
84
79
|
email: rack-devel@googlegroups.com
|
85
80
|
executables: []
|
86
|
-
|
87
81
|
extensions: []
|
88
|
-
|
89
|
-
extra_rdoc_files:
|
82
|
+
extra_rdoc_files:
|
90
83
|
- README.rdoc
|
91
84
|
- COPYING
|
92
|
-
files:
|
85
|
+
files:
|
93
86
|
- AUTHORS
|
94
87
|
- COPYING
|
95
88
|
- README.rdoc
|
@@ -117,6 +110,7 @@ files:
|
|
117
110
|
- lib/rack/contrib/not_found.rb
|
118
111
|
- lib/rack/contrib/post_body_content_type_parser.rb
|
119
112
|
- lib/rack/contrib/proctitle.rb
|
113
|
+
- lib/rack/contrib/printout.rb
|
120
114
|
- lib/rack/contrib/profiler.rb
|
121
115
|
- lib/rack/contrib/relative_redirect.rb
|
122
116
|
- lib/rack/contrib/response_cache.rb
|
@@ -168,46 +162,38 @@ files:
|
|
168
162
|
- test/spec_rack_static_cache.rb
|
169
163
|
- test/spec_rack_try_static.rb
|
170
164
|
- test/statics/test
|
171
|
-
has_rdoc: true
|
172
165
|
homepage: http://github.com/rack/rack-contrib/
|
173
|
-
licenses:
|
174
|
-
|
166
|
+
licenses:
|
167
|
+
- MIT
|
175
168
|
post_install_message:
|
176
|
-
rdoc_options:
|
169
|
+
rdoc_options:
|
177
170
|
- --line-numbers
|
178
171
|
- --inline-source
|
179
172
|
- --title
|
180
173
|
- rack-contrib
|
181
174
|
- --main
|
182
175
|
- README
|
183
|
-
require_paths:
|
176
|
+
require_paths:
|
184
177
|
- lib
|
185
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
186
179
|
none: false
|
187
|
-
requirements:
|
188
|
-
- -
|
189
|
-
- !ruby/object:Gem::Version
|
190
|
-
|
191
|
-
|
192
|
-
- 0
|
193
|
-
version: "0"
|
194
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ! '>='
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
185
|
none: false
|
196
|
-
requirements:
|
197
|
-
- -
|
198
|
-
- !ruby/object:Gem::Version
|
199
|
-
|
200
|
-
segments:
|
201
|
-
- 0
|
202
|
-
version: "0"
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
203
190
|
requirements: []
|
204
|
-
|
205
191
|
rubyforge_project:
|
206
|
-
rubygems_version: 1.
|
192
|
+
rubygems_version: 1.8.23
|
207
193
|
signing_key:
|
208
194
|
specification_version: 2
|
209
195
|
summary: Contributed Rack Middleware and Utilities
|
210
|
-
test_files:
|
196
|
+
test_files:
|
211
197
|
- test/spec_rack_accept_format.rb
|
212
198
|
- test/spec_rack_access.rb
|
213
199
|
- test/spec_rack_backstage.rb
|