rack-contrib 1.1.0 → 1.2.0

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.

Potentially problematic release.


This version of rack-contrib might be problematic. Click here for more details.

data/AUTHORS CHANGED
@@ -24,3 +24,4 @@ TJ Holowaychuk <tj@vision-media.ca>
24
24
  anupom syam <anupom.syam@gmail.com>
25
25
  ichverstehe <ichverstehe@gmail.com>
26
26
  kubicek <jiri@kubicek.cz>
27
+ Jordi Polo <mumismo@gmail.com>
data/COPYING CHANGED
@@ -1,3 +1,4 @@
1
+ The MIT License (MIT)
1
2
  Copyright (c) 2008 The Committers
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -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.org/rack/rack>
97
+ Rack On GitHub:: <http://github.com/rack/rack>
81
98
  rack-devel mailing list:: <http://groups.google.com/group/rack-devel>
@@ -3,7 +3,7 @@ require 'rack'
3
3
  module Rack
4
4
  module Contrib
5
5
  def self.release
6
- "1.0.1"
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+"#{extension}"
40
+ req.path_info = req.path_info.chomp('/') << "#{extension}"
41
41
  end
42
42
 
43
43
  @app.call(env)
@@ -19,10 +19,8 @@ module Rack
19
19
  private
20
20
 
21
21
  def domain
22
- @domain ||= begin
23
- @host =~ DOMAIN_REGEXP
24
- ".#{$1}.#{$2}"
25
- end
22
+ @host =~ DOMAIN_REGEXP
23
+ ".#{$1}.#{$2}"
26
24
  end
27
25
 
28
26
  def share_cookie(headers)
@@ -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
- response = pad(request.params.delete('callback'), response)
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{ |s| body << s.to_s }
60
- ["#{callback}(#{body})"]
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
@@ -8,24 +8,35 @@ module Rack
8
8
 
9
9
  def call(env)
10
10
  old_locale = I18n.locale
11
- locale = nil
12
-
13
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
14
- if lang = env["HTTP_ACCEPT_LANGUAGE"]
15
- lang = lang.split(",").map { |l|
16
- l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
17
- l.split(';q=')
18
- }.first
19
- locale = lang.first.split("-").first
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
- locale = env['rack.locale'] = I18n.locale = locale.to_s
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 'tmail'
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
- :server => 'localhost',
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 = TMail::Mail.new
57
- mail.to = Array(config[:to])
58
- mail.from = config[:from]
59
- mail.subject = config[:subject] % [exception.to_s]
60
- mail.date = Time.now
61
- mail.set_content_type 'text/plain'
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
- return if smtp[:server] == 'example.com'
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
- case env[CONTENT_TYPE]
33
- when APPLICATION_JSON
34
- env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
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
- process_time
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 = RubyProf::CallTreePrinter
20
- DEFAULT_CONTENT_TYPE = 'application/octet-stream'
12
+ DEFAULT_PRINTER = :call_stack
21
13
 
22
- PRINTER_CONTENT_TYPE = {
23
- RubyProf::FlatPrinter => 'text/plain',
24
- RubyProf::GraphPrinter => 'text/plain',
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] option
29
- # defaulting to :call_tree.
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' => PRINTER_CONTENT_TYPE[printer] || DEFAULT_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.nil?
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
- DEFAULT_PRINTER
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, /csss or /js from the
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
@@ -17,9 +17,9 @@ module Rack
17
17
 
18
18
  def initialize(app, options)
19
19
  @app = app
20
- @try = ['', *options.delete(:try)]
20
+ @try = ['', *options[:try]]
21
21
  @static = ::Rack::Static.new(
22
- lambda { [404, {}, []] },
22
+ lambda { |_| [404, {}, []] },
23
23
  options)
24
24
  end
25
25
 
@@ -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.1.0'
7
- s.date = '2010-10-19'
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
@@ -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 'tmail'
6
- require 'rack/contrib/mailexceptions'
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
- tmail = mailer.send(:generate_mail, test_exception, @env)
61
- tmail.to.should.equal ['foo@example.org']
62
- tmail.from.should.equal ['bar@example.org']
63
- tmail.subject.should.equal '[ERROR] Suffering Succotash!'
64
- tmail.body.should.not.be.nil
65
- tmail.body.should.be =~ /FOO:\s+"BAR"/
66
- tmail.body.should.be =~ /^\s*THE BODY\s*$/
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 (tmail not installed)"
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 handle requests with POST body Content-Type of application/json" do
10
- app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, Rack::Request.new(env).POST] }
11
- env = env_for_post_with_headers('/', {'Content_Type'.upcase => 'application/json'}, {:body => "asdf", :status => "12"}.to_json)
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 change nothing when the POST body content type isn't application/json" do
18
- app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, Rack::Request.new(env).POST] }
19
- body = Rack::PostBodyContentTypeParser.new(app).call(Rack::MockRequest.env_for("/", {:method => 'POST', :params => "body=asdf&status=12"})).last
20
- body['body'].should.equal "asdf"
21
- body['status'].should.equal "12"
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 env_for_post_with_headers(path, headers, body)
27
- Rack::MockRequest.env_for(path, {:method => "POST", :input => body}.merge(headers))
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)"
@@ -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::CallTreePrinter' do
11
+ specify 'printer defaults to RubyProf::CallStackPrinter' do
12
12
  profiler = Rack::Profiler.new(nil)
13
- profiler.instance_variable_get('@printer').should.equal RubyProf::CallTreePrinter
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 request(options = {})
8
- options.merge!({
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
- hash: 19
5
- prerelease: false
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
- date: 2010-10-19 00:00:00 -07:00
19
- default_executable:
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
- prerelease: false
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
- requirement: &id002 !ruby/object:Gem::Requirement
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
- requirement: &id003 !ruby/object:Gem::Requirement
40
+ version_requirements: !ruby/object:Gem::Requirement
57
41
  none: false
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- hash: 11
62
- segments:
63
- - 1
64
- - 2
65
- version: "1.2"
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
- requirement: &id004 !ruby/object:Gem::Requirement
56
+ version_requirements: !ruby/object:Gem::Requirement
72
57
  none: false
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- hash: 13
77
- segments:
78
- - 1
79
- - 1
80
- version: "1.1"
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
- version_requirements: *id004
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
- hash: 3
191
- segments:
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
- hash: 3
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.3.7
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