rack-contrib 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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