rack-contrib 1.8.0 → 2.3.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.

Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +17 -10
  3. data/lib/rack/contrib.rb +3 -3
  4. data/lib/rack/contrib/access.rb +6 -4
  5. data/lib/rack/contrib/backstage.rb +3 -1
  6. data/lib/rack/contrib/bounce_favicon.rb +2 -0
  7. data/lib/rack/contrib/callbacks.rb +2 -0
  8. data/lib/rack/contrib/common_cookies.rb +16 -11
  9. data/lib/rack/contrib/config.rb +3 -15
  10. data/lib/rack/contrib/cookies.rb +2 -0
  11. data/lib/rack/contrib/csshttprequest.rb +10 -6
  12. data/lib/rack/contrib/deflect.rb +34 -32
  13. data/lib/rack/contrib/enforce_valid_encoding.rb +2 -0
  14. data/lib/rack/contrib/evil.rb +2 -0
  15. data/lib/rack/contrib/expectation_cascade.rb +3 -1
  16. data/lib/rack/contrib/garbagecollector.rb +2 -0
  17. data/lib/rack/contrib/host_meta.rb +2 -0
  18. data/lib/rack/contrib/json_body_parser.rb +85 -0
  19. data/lib/rack/contrib/jsonp.rb +8 -6
  20. data/lib/rack/contrib/lazy_conditional_get.rb +9 -0
  21. data/lib/rack/contrib/lighttpd_script_name_fix.rb +2 -0
  22. data/lib/rack/contrib/locale.rb +65 -30
  23. data/lib/rack/contrib/mailexceptions.rb +4 -2
  24. data/lib/rack/contrib/nested_params.rb +6 -121
  25. data/lib/rack/contrib/not_found.rb +3 -1
  26. data/lib/rack/contrib/post_body_content_type_parser.rb +49 -4
  27. data/lib/rack/contrib/printout.rb +2 -0
  28. data/lib/rack/contrib/proctitle.rb +2 -0
  29. data/lib/rack/contrib/profiler.rb +6 -1
  30. data/lib/rack/contrib/relative_redirect.rb +10 -5
  31. data/lib/rack/contrib/response_cache.rb +22 -11
  32. data/lib/rack/contrib/response_headers.rb +2 -0
  33. data/lib/rack/contrib/route_exceptions.rb +2 -0
  34. data/lib/rack/contrib/runtime.rb +3 -30
  35. data/lib/rack/contrib/signals.rb +6 -0
  36. data/lib/rack/contrib/simple_endpoint.rb +3 -1
  37. data/lib/rack/contrib/static_cache.rb +17 -8
  38. data/lib/rack/contrib/time_zone.rb +2 -0
  39. data/lib/rack/contrib/try_static.rb +2 -0
  40. metadata +50 -32
  41. data/lib/rack/contrib/accept_format.rb +0 -66
  42. data/lib/rack/contrib/sendfile.rb +0 -138
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
 
3
5
  # A Rack middleware for providing JSON-P support.
@@ -53,7 +55,7 @@ module Rack
53
55
 
54
56
  # Set new Content-Length, if it was set before we mutated the response body
55
57
  if headers['Content-Length']
56
- length = response.to_ary.inject(0) { |len, part| len + bytesize(part) }
58
+ length = response.map(&:bytesize).reduce(0, :+)
57
59
  headers['Content-Length'] = length.to_s
58
60
  end
59
61
  end
@@ -87,8 +89,8 @@ module Rack
87
89
  # method of combining all of the data into a single string makes sense
88
90
  # since JSON is returned as a full string.
89
91
  #
90
- def pad(callback, response, body = "")
91
- response.each do |s|
92
+ def pad(callback, response)
93
+ body = response.to_enum.map do |s|
92
94
  # U+2028 and U+2029 are allowed inside strings in JSON (as all literal
93
95
  # Unicode characters) but JavaScript defines them as newline
94
96
  # seperators. Because no literal newlines are allowed in a string, this
@@ -96,8 +98,8 @@ module Rack
96
98
  # replacing them with the escaped version. This should be safe because
97
99
  # according to the JSON spec, these characters are *only* valid inside
98
100
  # a string and should therefore not be present any other places.
99
- body << s.to_s.gsub(U2028, '\u2028').gsub(U2029, '\u2029')
100
- end
101
+ s.gsub(U2028, '\u2028').gsub(U2029, '\u2029')
102
+ end.join
101
103
 
102
104
  # https://github.com/rack/rack-contrib/issues/46
103
105
  response.close if response.respond_to?(:close)
@@ -106,7 +108,7 @@ module Rack
106
108
  end
107
109
 
108
110
  def bad_request(body = "Bad Request")
109
- [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s }, [body] ]
111
+ [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.bytesize.to_s }, [body] ]
110
112
  end
111
113
 
112
114
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
 
3
5
  ##
@@ -43,6 +45,10 @@ module Rack
43
45
  # know for sure that it does not modify the cached content, you can set the
44
46
  # `Rack-Lazy-Conditional-Get` on response to `skip`. This will not update the
45
47
  # global modification date.
48
+ #
49
+ # NOTE: This will not work properly in a multi-threaded environment with
50
+ # default cache object. A provided cache object should ensure thread-safety
51
+ # of the `get`/`set`/`[]`/`[]=` methods.
46
52
 
47
53
  class LazyConditionalGet
48
54
 
@@ -70,7 +76,10 @@ module Rack
70
76
  if reading? env and fresh? env
71
77
  return [304, {'Last-Modified' => env['HTTP_IF_MODIFIED_SINCE']}, []]
72
78
  end
79
+
73
80
  status, headers, body = @app.call env
81
+ headers = Utils::HeaderHash.new(headers)
82
+
74
83
  update_cache unless (reading?(env) or skipping?(headers))
75
84
  headers['Last-Modified'] = cached_value if stampable? headers
76
85
  [status, headers, body]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
3
5
  # FastCGI app at "/". This middleware fixes this issue.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'i18n'
2
4
 
3
5
  module Rack
@@ -7,43 +9,76 @@ module Rack
7
9
  end
8
10
 
9
11
  def call(env)
10
- old_locale = I18n.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 unless headers['Content-Language']
17
- [status, headers, body]
18
- ensure
19
- I18n.locale = old_locale
12
+ locale_to_restore = I18n.locale
13
+
14
+ locale = user_preferred_locale(env["HTTP_ACCEPT_LANGUAGE"])
15
+ locale ||= I18n.default_locale
16
+
17
+ env['rack.locale'] = I18n.locale = locale.to_s
18
+ status, headers, body = @app.call(env)
19
+ headers = Utils::HeaderHash.new(headers)
20
+
21
+ unless headers['Content-Language']
22
+ headers['Content-Language'] = locale.to_s
20
23
  end
24
+
25
+ [status, headers, body]
26
+ ensure
27
+ I18n.locale = locale_to_restore
21
28
  end
22
29
 
23
30
  private
24
31
 
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
- language_and_qvalue = languages_and_qvalues.sort_by { |(locale, qvalue)|
36
- qvalue.to_f
37
- }.reverse.detect { |(locale, qvalue)|
38
- if I18n.enforce_available_locales
39
- locale == '*' || I18n.available_locales.include?(locale.to_sym)
40
- else
41
- true
32
+ # Accept-Language header is covered mainly by RFC 7231
33
+ # https://tools.ietf.org/html/rfc7231
34
+ #
35
+ # Related sections:
36
+ #
37
+ # * https://tools.ietf.org/html/rfc7231#section-5.3.1
38
+ # * https://tools.ietf.org/html/rfc7231#section-5.3.5
39
+ # * https://tools.ietf.org/html/rfc4647#section-3.4
40
+ #
41
+ # There is an obsolete RFC 2616 (https://tools.ietf.org/html/rfc2616)
42
+ #
43
+ # Edge cases:
44
+ #
45
+ # * Value can be a comma separated list with optional whitespaces:
46
+ # Accept-Language: da, en-gb;q=0.8, en;q=0.7
47
+ #
48
+ # * Quality value can contain optional whitespaces as well:
49
+ # Accept-Language: ru-UA, ru; q=0.8, uk; q=0.6, en-US; q=0.4, en; q=0.2
50
+ #
51
+ # * Quality prefix 'q=' can be in upper case (Q=)
52
+ #
53
+ # * Ignore case when match locale with I18n available locales
54
+ #
55
+ def user_preferred_locale(header)
56
+ return if header.nil?
57
+
58
+ locales = header.gsub(/\s+/, '').split(",").map do |language_tag|
59
+ locale, quality = language_tag.split(/;q=/i)
60
+ quality = quality ? quality.to_f : 1.0
61
+ [locale, quality]
62
+ end.reject do |(locale, quality)|
63
+ locale == '*' || quality == 0
64
+ end.sort_by do |(_, quality)|
65
+ quality
66
+ end.map(&:first)
67
+
68
+ return if locales.empty?
69
+
70
+ if I18n.enforce_available_locales
71
+ locale = locales.reverse.find { |locale| I18n.available_locales.any? { |al| match?(al, locale) } }
72
+ if locale
73
+ I18n.available_locales.find { |al| match?(al, locale) }
42
74
  end
43
- }
75
+ else
76
+ locales.last
77
+ end
78
+ end
44
79
 
45
- lang = language_and_qvalue && language_and_qvalue.first
46
- lang == '*' ? nil : lang
80
+ def match?(s1, s2)
81
+ s1.to_s.casecmp(s2.to_s) == 0
47
82
  end
48
83
  end
49
84
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'net/smtp'
2
4
  require 'mail'
3
5
  require 'erb'
@@ -90,7 +92,7 @@ module Rack
90
92
  mail.delivery_method :test
91
93
  elsif config[:smtp]
92
94
  smtp = config[:smtp]
93
- # for backward compability, replace the :server key with :address
95
+ # for backward compatibility, replace the :server key with :address
94
96
  address = smtp.delete :server
95
97
  smtp[:address] = address if address
96
98
  mail.delivery_method :smtp, smtp
@@ -104,7 +106,7 @@ module Rack
104
106
 
105
107
  def extract_body(env)
106
108
  if io = env['rack.input']
107
- io.rewind if io.respond_to?(:rewind)
109
+ io.rewind
108
110
  io.read
109
111
  end
110
112
  end
@@ -1,5 +1,6 @@
1
- require 'cgi'
2
- require 'strscan'
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/utils'
3
4
 
4
5
  module Rack
5
6
  # Rack middleware for parsing POST/PUT body data into nested parameters
@@ -20,130 +21,14 @@ module Rack
20
21
 
21
22
  def call(env)
22
23
  if form_vars = env[FORM_VARS]
23
- env[FORM_HASH] = parse_query_parameters(form_vars)
24
+ Rack::Utils.parse_nested_query(form_vars)
24
25
  elsif env[CONTENT_TYPE] == URL_ENCODED
25
26
  post_body = env[POST_BODY]
26
27
  env[FORM_INPUT] = post_body
27
- env[FORM_HASH] = parse_query_parameters(post_body.read)
28
- post_body.rewind if post_body.respond_to?(:rewind)
28
+ env[FORM_HASH] = Rack::Utils.parse_nested_query(post_body.read)
29
+ post_body.rewind
29
30
  end
30
31
  @app.call(env)
31
32
  end
32
-
33
- ## the rest is nabbed from Rails ##
34
-
35
- def parse_query_parameters(query_string)
36
- return {} if query_string.nil? or query_string.empty?
37
-
38
- pairs = query_string.split('&').collect do |chunk|
39
- next if chunk.empty?
40
- key, value = chunk.split('=', 2)
41
- next if key.empty?
42
- value = value.nil? ? nil : CGI.unescape(value)
43
- [ CGI.unescape(key), value ]
44
- end.compact
45
-
46
- UrlEncodedPairParser.new(pairs).result
47
- end
48
-
49
- class UrlEncodedPairParser < StringScanner
50
- attr_reader :top, :parent, :result
51
-
52
- def initialize(pairs = [])
53
- super('')
54
- @result = {}
55
- pairs.each { |key, value| parse(key, value) }
56
- end
57
-
58
- KEY_REGEXP = %r{([^\[\]=&]+)}
59
- BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
60
-
61
- # Parse the query string
62
- def parse(key, value)
63
- self.string = key
64
- @top, @parent = result, nil
65
-
66
- # First scan the bare key
67
- key = scan(KEY_REGEXP) or return
68
- key = post_key_check(key)
69
-
70
- # Then scan as many nestings as present
71
- until eos?
72
- r = scan(BRACKETED_KEY_REGEXP) or return
73
- key = self[1]
74
- key = post_key_check(key)
75
- end
76
-
77
- bind(key, value)
78
- end
79
-
80
- private
81
- # After we see a key, we must look ahead to determine our next action. Cases:
82
- #
83
- # [] follows the key. Then the value must be an array.
84
- # = follows the key. (A value comes next)
85
- # & or the end of string follows the key. Then the key is a flag.
86
- # otherwise, a hash follows the key.
87
- def post_key_check(key)
88
- if scan(/\[\]/) # a[b][] indicates that b is an array
89
- container(key, Array)
90
- nil
91
- elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
92
- container(key, Hash)
93
- nil
94
- else # End of key? We do nothing.
95
- key
96
- end
97
- end
98
-
99
- # Add a container to the stack.
100
- def container(key, klass)
101
- type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
102
- value = bind(key, klass.new)
103
- type_conflict! klass, value unless value.is_a?(klass)
104
- push(value)
105
- end
106
-
107
- # Push a value onto the 'stack', which is actually only the top 2 items.
108
- def push(value)
109
- @parent, @top = @top, value
110
- end
111
-
112
- # Bind a key (which may be nil for items in an array) to the provided value.
113
- def bind(key, value)
114
- if top.is_a? Array
115
- if key
116
- if top[-1].is_a?(Hash) && ! top[-1].key?(key)
117
- top[-1][key] = value
118
- else
119
- top << {key => value}
120
- end
121
- push top.last
122
- return top[key]
123
- else
124
- top << value
125
- return value
126
- end
127
- elsif top.is_a? Hash
128
- key = CGI.unescape(key)
129
- parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
130
-
131
- if top.include?(key) && value.is_a?(String)
132
- warn "Rack::NestedParams' parsing behavior will change in rack-contrib 2.0"
133
- warn "Currently 'foo=1&foo=2' is parsed to {'foo' => '1'}, in 2.0 it will be parsed to {'foo' => '2'}"
134
- end
135
-
136
- top[key] ||= value
137
- return top[key]
138
- else
139
- raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
140
- end
141
- end
142
-
143
- def type_conflict!(klass, value)
144
- raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
145
- end
146
- end
147
-
148
33
  end
149
34
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack::NotFound is a default endpoint. Optionally initialize with the
3
5
  # path to a custom 404 page, to override the standard response body.
@@ -19,7 +21,7 @@ module Rack
19
21
  else
20
22
  @content = F.read(path)
21
23
  end
22
- @length = @content.size.to_s
24
+ @length = @content.bytesize.to_s
23
25
 
24
26
  @content_type = content_type
25
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'json'
3
5
  rescue LoadError => e
@@ -6,10 +8,51 @@ end
6
8
 
7
9
  module Rack
8
10
 
11
+ # <b>DEPRECATED:</b> <tt>JSONBodyParser</tt> is a drop-in replacement that is faster and more configurable.
12
+ #
9
13
  # A Rack middleware for parsing POST/PUT body data when Content-Type is
10
14
  # not one of the standard supported types, like <tt>application/json</tt>.
11
15
  #
12
- # TODO: Find a better name.
16
+ # === How to use the middleware
17
+ #
18
+ # Example of simple +config.ru+ file:
19
+ #
20
+ # require 'rack'
21
+ # require 'rack/contrib'
22
+ #
23
+ # use ::Rack::PostBodyContentTypeParser
24
+ #
25
+ # app = lambda do |env|
26
+ # request = Rack::Request.new(env)
27
+ # body = "Hello #{request.params['name']}"
28
+ # [200, {'Content-Type' => 'text/plain'}, [body]]
29
+ # end
30
+ #
31
+ # run app
32
+ #
33
+ # Example with passing block argument:
34
+ #
35
+ # use ::Rack::PostBodyContentTypeParser do |body|
36
+ # { 'params' => JSON.parse(body) }
37
+ # end
38
+ #
39
+ # Example with passing proc argument:
40
+ #
41
+ # parser = ->(body) { { 'params' => JSON.parse(body) } }
42
+ # use ::Rack::PostBodyContentTypeParser, &parser
43
+ #
44
+ #
45
+ # === Failed JSON parsing
46
+ #
47
+ # Returns "400 Bad request" response if invalid JSON document was sent:
48
+ #
49
+ # Raw HTTP response:
50
+ #
51
+ # HTTP/1.1 400 Bad Request
52
+ # Content-Type: text/plain
53
+ # Content-Length: 28
54
+ #
55
+ # failed to parse body as JSON
13
56
  #
14
57
  class PostBodyContentTypeParser
15
58
 
@@ -24,14 +67,16 @@ module Rack
24
67
  #
25
68
  APPLICATION_JSON = 'application/json'.freeze
26
69
 
27
- def initialize(app)
70
+ def initialize(app, &block)
71
+ warn "[DEPRECATION] `PostBodyContentTypeParser` is deprecated. Use `JSONBodyParser` as a drop-in replacement."
28
72
  @app = app
73
+ @block = block || Proc.new { |body| JSON.parse(body, :create_additions => false) }
29
74
  end
30
75
 
31
76
  def call(env)
32
77
  if Rack::Request.new(env).media_type == APPLICATION_JSON && (body = env[POST_BODY].read).length != 0
33
78
  env[POST_BODY].rewind # somebody might try to read this stream
34
- env.update(FORM_HASH => JSON.parse(body, :create_additions => false), FORM_INPUT => env[POST_BODY])
79
+ env.update(FORM_HASH => @block.call(body), FORM_INPUT => env[POST_BODY])
35
80
  end
36
81
  @app.call(env)
37
82
  rescue JSON::ParserError
@@ -39,7 +84,7 @@ module Rack
39
84
  end
40
85
 
41
86
  def bad_request(body = 'Bad Request')
42
- [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s }, [body] ]
87
+ [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.bytesize.to_s }, [body] ]
43
88
  end
44
89
  end
45
90
  end