rack-contrib 2.1.0 → 2.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c7731ec4011969a7792a075037754884383bd79288d308f842db8d3566d69f9
4
- data.tar.gz: 16ea5eb6e86c262eaf5382f7bd48d84a5d96f77730ef2afa3595da5ff63c01b7
3
+ metadata.gz: ca58db2b7968ad4691e8bec79eea2088dabc3da5d309358f7df76d668324d824
4
+ data.tar.gz: 51816bb6b59a598fea16eb100cc783b3c0ff58eb37aa3585bb8d65141f2aae4f
5
5
  SHA512:
6
- metadata.gz: ba48f0a22d13513a34589c096b87fa836fd762799dd46a05ee17e0f8b07ec8bf41489aaf59e18f63c59f7c8d53bee10e9e2b1492fa7a5a12b70587f4eb644e46
7
- data.tar.gz: ae7ca36ea51cfcb66053bd97baef862eb161f91fd08badce5283f05765263a354a2e9f5214e75ca4eb9f4fa5ad4505ad2a305d2899fca8d95736cd2fd847f945
6
+ metadata.gz: 7442b5ad170ae2946f017d8ccd007d02455b7939fe42239378505b61983cd9a1f090549e87b07e08323a7cf99d04f026e8df0f326e34be166508187cca3735e7
7
+ data.tar.gz: 99985100672c65c9ee559a37281f5dddea4fcdc8481ddffd75c2f6e697197a1070339c5b70733d736f8ba33078d5f699f00097cd6ec96266c8f07edb008b727b
data/README.md CHANGED
@@ -13,6 +13,7 @@ interface:
13
13
  * `Rack::Deflect` - Helps protect against DoS attacks.
14
14
  * `Rack::Evil` - Lets the rack application return a response to the client from any place.
15
15
  * `Rack::HostMeta` - Configures `/host-meta` using a block
16
+ * `Rack::JSONBodyParser` - Adds JSON request bodies to the Rack parameters hash.
16
17
  * `Rack::JSONP` - Adds JSON-P support by stripping out the callback param and padding the response with the appropriate callback format.
17
18
  * `Rack::LazyConditionalGet` - Caches a global `Last-Modified` date and updates it each time there is a request that is not `GET` or `HEAD`.
18
19
  * `Rack::LighttpdScriptNameFix` - Fixes how lighttpd sets the `SCRIPT_NAME` and `PATH_INFO` variables in certain configurations.
@@ -20,7 +21,7 @@ interface:
20
21
  * `Rack::MailExceptions` - Rescues exceptions raised from the app and sends a useful email with the exception, stacktrace, and contents of the environment.
21
22
  * `Rack::NestedParams` - parses form params with subscripts (e.g., * "`post[title]=Hello`") into a nested/recursive Hash structure (based on Rails' implementation).
22
23
  * `Rack::NotFound` - A default 404 application.
23
- * `Rack::PostBodyContentTypeParser` - Adds support for JSON request bodies. The Rack parameter hash is populated by deserializing the JSON data provided in the request body when the Content-Type is application/json.
24
+ * `Rack::PostBodyContentTypeParser` - [Deprecated]: Adds support for JSON request bodies. The Rack parameter hash is populated by deserializing the JSON data provided in the request body when the Content-Type is application/json
24
25
  * `Rack::Printout` - Prints the environment and the response per request
25
26
  * `Rack::ProcTitle` - Displays request information in process title (`$0`) for monitoring/inspection with ps(1).
26
27
  * `Rack::Profiler` - Uses ruby-prof to measure request time.
@@ -24,6 +24,7 @@ module Rack
24
24
  autoload :HostMeta, "rack/contrib/host_meta"
25
25
  autoload :GarbageCollector, "rack/contrib/garbagecollector"
26
26
  autoload :JSONP, "rack/contrib/jsonp"
27
+ autoload :JSONBodyParser, "rack/contrib/json_body_parser"
27
28
  autoload :LazyConditionalGet, "rack/contrib/lazy_conditional_get"
28
29
  autoload :LighttpdScriptNameFix, "rack/contrib/lighttpd_script_name_fix"
29
30
  autoload :Locale, "rack/contrib/locale"
@@ -31,7 +31,7 @@ module Rack
31
31
  end
32
32
 
33
33
  def modify_headers!(headers, encoded_response)
34
- headers['Content-Length'] = encoded_response.length.to_s
34
+ headers['Content-Length'] = encoded_response.bytesize.to_s
35
35
  headers['Content-Type'] = 'text/css'
36
36
  nil
37
37
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Rack
6
+ # A Rack middleware that makes JSON-encoded request bodies available in the
7
+ # request.params hash. By default it parses POST, PATCH, and PUT requests
8
+ # whose media type is <tt>application/json</tt>. You can configure it to match
9
+ # any verb or media type via the <tt>:verbs</tt> and <tt>:media</tt> options.
10
+ #
11
+ #
12
+ # == Examples:
13
+ #
14
+ # === Parse POST and GET requests only
15
+ # use Rack::JSONBodyParser, verbs: ['POST', 'GET']
16
+ #
17
+ # === Parse POST|PATCH|PUT requests whose Content-Type matches 'json'
18
+ # use Rack::JSONBodyParser, media: /json/
19
+ #
20
+ # === Parse POST requests whose Content-Type is 'application/json' or 'application/vnd+json'
21
+ # use Rack::JSONBodyParser, verbs: ['POST'], media: ['application/json', 'application/vnd.api+json']
22
+ #
23
+ class JSONBodyParser
24
+ CONTENT_TYPE_MATCHERS = {
25
+ String => lambda { |option, header|
26
+ Rack::MediaType.type(header) == option
27
+ },
28
+ Array => lambda { |options, header|
29
+ media_type = Rack::MediaType.type(header)
30
+ options.any? { |opt| media_type == opt }
31
+ },
32
+ Regexp => lambda {
33
+ if //.respond_to?(:match?)
34
+ # Use Ruby's fast regex matcher when available
35
+ ->(option, header) { option.match? header }
36
+ else
37
+ # Fall back to the slower matcher for rubies older than 2.4
38
+ ->(option, header) { option.match header }
39
+ end
40
+ }.call(),
41
+ }.freeze
42
+
43
+ DEFAULT_PARSER = ->(body) { JSON.parse(body, create_additions: false) }
44
+
45
+ def initialize(
46
+ app,
47
+ verbs: %w[POST PATCH PUT],
48
+ media: 'application/json',
49
+ &block
50
+ )
51
+ @app = app
52
+ @verbs, @media = verbs, media
53
+ @matcher = CONTENT_TYPE_MATCHERS.fetch(@media.class)
54
+ @parser = block || DEFAULT_PARSER
55
+ end
56
+
57
+ def call(env)
58
+ if @verbs.include?(env[Rack::REQUEST_METHOD]) &&
59
+ @matcher.call(@media, env['CONTENT_TYPE'])
60
+
61
+ update_form_hash_with_json_body(env)
62
+ end
63
+ @app.call(env)
64
+ rescue JSON::ParserError
65
+ body = { error: 'Failed to parse body as JSON' }.to_json
66
+ header = { 'Content-Type' => 'application/json' }
67
+ Rack::Response.new(body, 400, header).finish
68
+ end
69
+
70
+ private
71
+
72
+ def update_form_hash_with_json_body(env)
73
+ body = env[Rack::RACK_INPUT]
74
+ return unless (body_content = body.read) && !body_content.empty?
75
+
76
+ body.rewind # somebody might try to read this stream
77
+ env.update(
78
+ Rack::RACK_REQUEST_FORM_HASH => @parser.call(body_content),
79
+ Rack::RACK_REQUEST_FORM_INPUT => body
80
+ )
81
+ end
82
+ end
83
+ end
@@ -106,7 +106,7 @@ module Rack
106
106
  end
107
107
 
108
108
  def bad_request(body = "Bad Request")
109
- [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s }, [body] ]
109
+ [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.bytesize.to_s }, [body] ]
110
110
  end
111
111
 
112
112
  end
@@ -7,43 +7,75 @@ module Rack
7
7
  end
8
8
 
9
9
  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
10
+ locale_to_restore = I18n.locale
11
+
12
+ locale = user_preferred_locale(env["HTTP_ACCEPT_LANGUAGE"])
13
+ locale ||= I18n.default_locale
14
+
15
+ env['rack.locale'] = I18n.locale = locale.to_s
16
+ status, headers, body = @app.call(env)
17
+
18
+ unless headers['Content-Language']
19
+ headers['Content-Language'] = locale.to_s
20
20
  end
21
+
22
+ [status, headers, body]
23
+ ensure
24
+ I18n.locale = locale_to_restore
21
25
  end
22
26
 
23
27
  private
24
28
 
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
29
+ # Accept-Language header is covered mainly by RFC 7231
30
+ # https://tools.ietf.org/html/rfc7231
31
+ #
32
+ # Related sections:
33
+ #
34
+ # * https://tools.ietf.org/html/rfc7231#section-5.3.1
35
+ # * https://tools.ietf.org/html/rfc7231#section-5.3.5
36
+ # * https://tools.ietf.org/html/rfc4647#section-3.4
37
+ #
38
+ # There is an obsolete RFC 2616 (https://tools.ietf.org/html/rfc2616)
39
+ #
40
+ # Edge cases:
41
+ #
42
+ # * Value can be a comma separated list with optional whitespaces:
43
+ # Accept-Language: da, en-gb;q=0.8, en;q=0.7
44
+ #
45
+ # * Quality value can contain optional whitespaces as well:
46
+ # Accept-Language: ru-UA, ru; q=0.8, uk; q=0.6, en-US; q=0.4, en; q=0.2
47
+ #
48
+ # * Quality prefix 'q=' can be in upper case (Q=)
49
+ #
50
+ # * Ignore case when match locale with I18n available locales
51
+ #
52
+ def user_preferred_locale(header)
53
+ return if header.nil?
54
+
55
+ locales = header.gsub(/\s+/, '').split(",").map do |language_tag|
56
+ locale, quality = language_tag.split(/;q=/i)
57
+ quality = quality ? quality.to_f : 1.0
58
+ [locale, quality]
59
+ end.reject do |(locale, quality)|
60
+ locale == '*' || quality == 0
61
+ end.sort_by do |(_, quality)|
62
+ quality
63
+ end.map(&:first)
64
+
65
+ return if locales.empty?
66
+
67
+ if I18n.enforce_available_locales
68
+ locale = locales.reverse.find { |locale| I18n.available_locales.any? { |al| match?(al, locale) } }
69
+ if locale
70
+ I18n.available_locales.find { |al| match?(al, locale) }
42
71
  end
43
- }
72
+ else
73
+ locales.last
74
+ end
75
+ end
44
76
 
45
- lang = language_and_qvalue && language_and_qvalue.first
46
- lang == '*' ? nil : lang
77
+ def match?(s1, s2)
78
+ s1.to_s.casecmp(s2.to_s) == 0
47
79
  end
48
80
  end
49
81
  end
@@ -6,10 +6,51 @@ end
6
6
 
7
7
  module Rack
8
8
 
9
+ # <b>DEPRECATED:</b> <tt>JSONBodyParser</tt> is a drop-in replacement that is faster and more configurable.
10
+ #
9
11
  # A Rack middleware for parsing POST/PUT body data when Content-Type is
10
12
  # not one of the standard supported types, like <tt>application/json</tt>.
11
13
  #
12
- # TODO: Find a better name.
14
+ # === How to use the middleware
15
+ #
16
+ # Example of simple +config.ru+ file:
17
+ #
18
+ # require 'rack'
19
+ # require 'rack/contrib'
20
+ #
21
+ # use ::Rack::PostBodyContentTypeParser
22
+ #
23
+ # app = lambda do |env|
24
+ # request = Rack::Request.new(env)
25
+ # body = "Hello #{request.params['name']}"
26
+ # [200, {'Content-Type' => 'text/plain'}, [body]]
27
+ # end
28
+ #
29
+ # run app
30
+ #
31
+ # Example with passing block argument:
32
+ #
33
+ # use ::Rack::PostBodyContentTypeParser do |body|
34
+ # { 'params' => JSON.parse(body) }
35
+ # end
36
+ #
37
+ # Example with passing proc argument:
38
+ #
39
+ # parser = ->(body) { { 'params' => JSON.parse(body) } }
40
+ # use ::Rack::PostBodyContentTypeParser, &parser
41
+ #
42
+ #
43
+ # === Failed JSON parsing
44
+ #
45
+ # Returns "400 Bad request" response if invalid JSON document was sent:
46
+ #
47
+ # Raw HTTP response:
48
+ #
49
+ # HTTP/1.1 400 Bad Request
50
+ # Content-Type: text/plain
51
+ # Content-Length: 28
52
+ #
53
+ # failed to parse body as JSON
13
54
  #
14
55
  class PostBodyContentTypeParser
15
56
 
@@ -25,6 +66,7 @@ module Rack
25
66
  APPLICATION_JSON = 'application/json'.freeze
26
67
 
27
68
  def initialize(app, &block)
69
+ warn "[DEPRECATION] `PostBodyContentTypeParser` is deprecated. Use `JSONBodyParser` as a drop-in replacement."
28
70
  @app = app
29
71
  @block = block || Proc.new { |body| JSON.parse(body, :create_additions => false) }
30
72
  end
@@ -40,7 +82,7 @@ module Rack
40
82
  end
41
83
 
42
84
  def bad_request(body = 'Bad Request')
43
- [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s }, [body] ]
85
+ [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.bytesize.to_s }, [body] ]
44
86
  end
45
87
  end
46
88
  end
@@ -3,9 +3,12 @@ require 'rack'
3
3
 
4
4
  # Rack::ResponseCache is a Rack middleware that caches responses for successful
5
5
  # GET requests with no query string to disk or any ruby object that has an
6
- # []= method (so it works with memcached). When caching to disk, it works similar to
7
- # Rails' page caching, allowing you to cache dynamic pages to static files that can
8
- # be served directly by a front end webserver.
6
+ # []= method (so it works with memcached). As with Rails' page caching, this
7
+ # middleware only writes to the cache -- it never reads. The logic of whether a
8
+ # cached response should be served is left either to your web server, via
9
+ # something like the <tt>try_files</tt> directive in nginx, or to your
10
+ # cache-reading middleware of choice, mounted before Rack::ResponseCache in the
11
+ # stack.
9
12
  class Rack::ResponseCache
10
13
  # The default proc used if a block is not provided to .new
11
14
  # It unescapes the PATH_INFO of the environment, and makes sure that it doesn't
@@ -15,7 +18,14 @@ class Rack::ResponseCache
15
18
  # of the path to index.html.
16
19
  DEFAULT_PATH_PROC = proc do |env, res|
17
20
  path = Rack::Utils.unescape(env['PATH_INFO'])
18
- if !path.include?('..') and match = /text\/((?:x|ht)ml|css)/o.match(res[1]['Content-Type'])
21
+ headers = res[1]
22
+ # Content-Type is almost always at headers['Content-Type'], but to fully
23
+ # comply with HTTP RFC 7230, we fall back to a case-insensitive lookup
24
+ content_type = headers.fetch('Content-Type') do |titlecase_key|
25
+ _, val = headers.find { |key, _| key.casecmp(titlecase_key) == 0 }
26
+ val
27
+ end
28
+ if !path.include?('..') and match = /text\/((?:x|ht)ml|css)/o.match(content_type)
19
29
  type = match[1]
20
30
  path = "#{path}.#{type}" unless /\.#{type}\z/.match(path)
21
31
  path = File.join(File.dirname(path), 'index.html') if type == 'html' and File.basename(path) == '.html'
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  module Rack
2
4
 
3
5
  #
@@ -68,7 +70,6 @@ module Rack
68
70
  @version_regex = options.fetch(:version_regex, /-[\d.]+([.][a-zA-Z][\w]+)?$/)
69
71
  end
70
72
  @duration_in_seconds = self.duration_in_seconds
71
- @duration_in_words = self.duration_in_words
72
73
  end
73
74
 
74
75
  def call(env)
@@ -83,14 +84,15 @@ module Rack
83
84
  status, headers, body = @file_server.call(env)
84
85
  if @no_cache[url].nil?
85
86
  headers['Cache-Control'] ="max-age=#{@duration_in_seconds}, public"
86
- headers['Expires'] = @duration_in_words
87
+ headers['Expires'] = duration_in_words
87
88
  end
89
+ headers['Date'] = Time.now.httpdate
88
90
  [status, headers, body]
89
91
  end
90
92
  end
91
93
 
92
94
  def duration_in_words
93
- (Time.now + self.duration_in_seconds).strftime '%a, %d %b %Y %H:%M:%S GMT'
95
+ (Time.now.utc + self.duration_in_seconds).httpdate
94
96
  end
95
97
 
96
98
  def duration_in_seconds
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-contrib
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rack-devel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-04 00:00:00.000000000 Z
11
+ date: 2020-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.0'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3'
34
37
  type: :development
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '1.0'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: git-version-bump
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -224,6 +230,20 @@ dependencies:
224
230
  - - "~>"
225
231
  - !ruby/object:Gem::Version
226
232
  version: '0.17'
233
+ - !ruby/object:Gem::Dependency
234
+ name: timecop
235
+ requirement: !ruby/object:Gem::Requirement
236
+ requirements:
237
+ - - "~>"
238
+ - !ruby/object:Gem::Version
239
+ version: '0.9'
240
+ type: :development
241
+ prerelease: false
242
+ version_requirements: !ruby/object:Gem::Requirement
243
+ requirements:
244
+ - - "~>"
245
+ - !ruby/object:Gem::Version
246
+ version: '0.9'
227
247
  description: Contributed Rack Middleware and Utilities
228
248
  email: rack-devel@googlegroups.com
229
249
  executables: []
@@ -250,6 +270,7 @@ files:
250
270
  - lib/rack/contrib/expectation_cascade.rb
251
271
  - lib/rack/contrib/garbagecollector.rb
252
272
  - lib/rack/contrib/host_meta.rb
273
+ - lib/rack/contrib/json_body_parser.rb
253
274
  - lib/rack/contrib/jsonp.rb
254
275
  - lib/rack/contrib/lazy_conditional_get.rb
255
276
  - lib/rack/contrib/lighttpd_script_name_fix.rb
@@ -296,8 +317,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
296
317
  - !ruby/object:Gem::Version
297
318
  version: '0'
298
319
  requirements: []
299
- rubyforge_project:
300
- rubygems_version: 2.7.7
320
+ rubygems_version: 3.0.3
301
321
  signing_key:
302
322
  specification_version: 2
303
323
  summary: Contributed Rack Middleware and Utilities