rack-contrib 2.0.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +12 -8
  3. data/lib/rack/contrib/access.rb +8 -6
  4. data/lib/rack/contrib/backstage.rb +5 -3
  5. data/lib/rack/contrib/bounce_favicon.rb +3 -1
  6. data/lib/rack/contrib/callbacks.rb +2 -0
  7. data/lib/rack/contrib/common_cookies.rb +19 -11
  8. data/lib/rack/contrib/config.rb +3 -15
  9. data/lib/rack/contrib/cookies.rb +2 -0
  10. data/lib/rack/contrib/csshttprequest.rb +12 -6
  11. data/lib/rack/contrib/deflect.rb +35 -33
  12. data/lib/rack/contrib/enforce_valid_encoding.rb +3 -1
  13. data/lib/rack/contrib/evil.rb +2 -0
  14. data/lib/rack/contrib/expectation_cascade.rb +5 -3
  15. data/lib/rack/contrib/garbagecollector.rb +2 -0
  16. data/lib/rack/contrib/host_meta.rb +3 -1
  17. data/lib/rack/contrib/json_body_parser.rb +94 -0
  18. data/lib/rack/contrib/jsonp.rb +12 -7
  19. data/lib/rack/contrib/lazy_conditional_get.rb +13 -4
  20. data/lib/rack/contrib/lighttpd_script_name_fix.rb +2 -0
  21. data/lib/rack/contrib/locale.rb +75 -30
  22. data/lib/rack/contrib/mailexceptions.rb +2 -0
  23. data/lib/rack/contrib/nested_params.rb +3 -1
  24. data/lib/rack/contrib/not_found.rb +3 -1
  25. data/lib/rack/contrib/post_body_content_type_parser.rb +50 -5
  26. data/lib/rack/contrib/printout.rb +3 -1
  27. data/lib/rack/contrib/proctitle.rb +2 -0
  28. data/lib/rack/contrib/profiler.rb +44 -17
  29. data/lib/rack/contrib/relative_redirect.rb +11 -5
  30. data/lib/rack/contrib/response_cache.rb +21 -9
  31. data/lib/rack/contrib/response_headers.rb +8 -2
  32. data/lib/rack/contrib/route_exceptions.rb +2 -0
  33. data/lib/rack/contrib/runtime.rb +3 -30
  34. data/lib/rack/contrib/signals.rb +6 -0
  35. data/lib/rack/contrib/simple_endpoint.rb +3 -1
  36. data/lib/rack/contrib/static_cache.rb +18 -7
  37. data/lib/rack/contrib/time_zone.rb +2 -0
  38. data/lib/rack/contrib/try_static.rb +2 -0
  39. data/lib/rack/contrib/version.rb +5 -0
  40. data/lib/rack/contrib.rb +3 -1
  41. metadata +13 -212
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7784c9ef2deb5524c67848eec58934752c586e8d
4
- data.tar.gz: 76bca1883c124f0c9143ef9b0d79cb550c261c03
2
+ SHA256:
3
+ metadata.gz: 1b83b23d3e67c0a24d65f6da2406e8430b003100bae5b5e8f3b20486384804e0
4
+ data.tar.gz: 4e2b5a6b94dbd7be2b10a6dae026509166d236e0b0e842f0a127b59d28298ae9
5
5
  SHA512:
6
- metadata.gz: b9a70c52bcf05c8c56a11f7822e8b297feafcccd4cf67e80f21f3a5ea64116aab9db8a57e942c5eaea92825476fa7f1f9b7ad4b4133b1f8632e1a78f59f71f38
7
- data.tar.gz: bced2fb275258c226af75d670c6fac2eb5a2d10422ae0c9d9026f19330cf635dda62449e055923669a2c7495827ecb8035cdbcf849068e4a11f2ed46c980aa8d
6
+ metadata.gz: a12773ffdd9646740002b8124c2bd02bdc92d738ced7c4bb270e6003661a259d4dc8e34925bd909805bd1e40a0d381b9efa59c3d2d02d1fe5f9dcd57af8809c9
7
+ data.tar.gz: c9955bcfb8787a930d9590cc984ab16a25211e2f52b9fe76b7d357088bbc68503e1c2fc362c46ba0c8febb08aa73eb67ed04bd03ddbdea39bd489e336211d385
data/README.md CHANGED
@@ -3,17 +3,16 @@
3
3
  This package includes a variety of add-on components for Rack, a Ruby web server
4
4
  interface:
5
5
 
6
- * `Rack::AcceptFormat` - Adds a format extension at the end of the URI when there is none, corresponding to the mime-type given in the Accept HTTP header.
7
6
  * `Rack::Access` - Limits access based on IP address
8
7
  * `Rack::Backstage` - Returns content of specified file if it exists, which makes it convenient for putting up maintenance pages.
9
8
  * `Rack::BounceFavicon` - Returns a 404 for requests to `/favicon.ico`
10
9
  * `Rack::CSSHTTPRequest` - Adds CSSHTTPRequest support by encoding responses as CSS for cross-site AJAX-style data loading
11
10
  * `Rack::Callbacks` - Implements DSL for pure before/after filter like Middlewares.
12
- * `Rack::Config` - Shared configuration for cooperative middleware.
13
11
  * `Rack::Cookies` - Adds simple cookie jar hash to env
14
12
  * `Rack::Deflect` - Helps protect against DoS attacks.
15
13
  * `Rack::Evil` - Lets the rack application return a response to the client from any place.
16
14
  * `Rack::HostMeta` - Configures `/host-meta` using a block
15
+ * `Rack::JSONBodyParser` - Adds JSON request bodies to the Rack parameters hash.
17
16
  * `Rack::JSONP` - Adds JSON-P support by stripping out the callback param and padding the response with the appropriate callback format.
18
17
  * `Rack::LazyConditionalGet` - Caches a global `Last-Modified` date and updates it each time there is a request that is not `GET` or `HEAD`.
19
18
  * `Rack::LighttpdScriptNameFix` - Fixes how lighttpd sets the `SCRIPT_NAME` and `PATH_INFO` variables in certain configurations.
@@ -21,14 +20,13 @@ interface:
21
20
  * `Rack::MailExceptions` - Rescues exceptions raised from the app and sends a useful email with the exception, stacktrace, and contents of the environment.
22
21
  * `Rack::NestedParams` - parses form params with subscripts (e.g., * "`post[title]=Hello`") into a nested/recursive Hash structure (based on Rails' implementation).
23
22
  * `Rack::NotFound` - A default 404 application.
24
- * `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.
23
+ * `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
25
24
  * `Rack::Printout` - Prints the environment and the response per request
26
25
  * `Rack::ProcTitle` - Displays request information in process title (`$0`) for monitoring/inspection with ps(1).
27
26
  * `Rack::Profiler` - Uses ruby-prof to measure request time.
28
27
  * `Rack::RelativeRedirect` - Transforms relative paths in redirects to absolute URLs.
29
- * `Rack::ResponseCache` - Caches responses to requests without query strings to Disk or a user provider Ruby object. Similar to Rails' page caching.
28
+ * `Rack::ResponseCache` - Caches responses to requests without query strings to Disk or a user provided Ruby object. Similar to Rails' page caching.
30
29
  * `Rack::ResponseHeaders` - Manipulates response headers object at runtime
31
- * `Rack::Sendfile` - Enables `X-Sendfile` support for bodies that can be served from file.
32
30
  * `Rack::Signals` - Installs signal handlers that are safely processed after a request
33
31
  * `Rack::SimpleEndpoint` - Creates simple endpoints with routing rules, similar to Sinatra actions
34
32
  * `Rack::StaticCache` - Modifies the response headers to facilitiate client and proxy caching for static files that minimizes http requests and improves overall load times for second time visitors.
@@ -78,7 +76,7 @@ To contribute to the project, begin by cloning the repo and installing the neces
78
76
 
79
77
  gem install json rack ruby-prof test-spec test-unit
80
78
 
81
- To run the entire test suite, run
79
+ To run the entire test suite, run
82
80
 
83
81
  rake test
84
82
 
@@ -86,7 +84,7 @@ To run a specific component's tests run
86
84
 
87
85
  specrb -Ilib:test -w test/spec_rack_thecomponent.rb
88
86
 
89
- This works on ruby 1.8.7 but has problems under ruby 1.9.x.
87
+ This works on ruby 1.8.7 but has problems under ruby 1.9.x.
90
88
 
91
89
  TODO: instructions for 1.9.x and include bundler
92
90
 
@@ -100,10 +98,16 @@ The criteria for middleware being included in this project are roughly as follow
100
98
  These criteria were introduced several years after the start of the project, so some of the included middleware may not meet all of them. In particular, several middleware have external dependencies. It is possible that in some future release of rack-contrib, middleware with external depencies will be removed from the project.
101
99
 
102
100
  When submitting code keep the above criteria in mind and also see the code
103
- guidelines in CONTRIBUTING.md.
101
+ guidelines in CONTRIBUTING.md.
104
102
 
105
103
  ### Links
106
104
 
107
105
  * rack-contrib on GitHub:: <https://github.com/rack/rack-contrib>
108
106
  * Rack:: <https://rack.github.io/>
109
107
  * Rack On GitHub:: <https://github.com/rack/rack>
108
+
109
+
110
+ ### Security Reporting
111
+
112
+ To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
113
+ Tidelift will coordinate the fix and disclosure.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ipaddr"
2
4
 
3
5
  module Rack
@@ -38,7 +40,7 @@ module Rack
38
40
  raise ArgumentError, "paths need to start with /"
39
41
  end
40
42
  location = location.chomp('/')
41
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
43
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
42
44
 
43
45
  ipmasks.collect! do |ipmask|
44
46
  ipmask.is_a?(IPAddr) ? ipmask : IPAddr.new(ipmask)
@@ -48,9 +50,9 @@ module Rack
48
50
  end
49
51
 
50
52
  def call(env)
51
- @original_request = Request.new(env)
53
+ request = Request.new(env)
52
54
  ipmasks = ipmasks_for_path(env)
53
- return forbidden! unless ip_authorized?(ipmasks)
55
+ return forbidden! unless ip_authorized?(request, ipmasks)
54
56
  status, headers, body = @app.call(env)
55
57
  [status, headers, body]
56
58
  end
@@ -70,14 +72,14 @@ module Rack
70
72
  end
71
73
 
72
74
  def forbidden!
73
- [403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, []]
75
+ [403, { 'content-type' => 'text/html', 'content-length' => '0' }, []]
74
76
  end
75
77
 
76
- def ip_authorized?(ipmasks)
78
+ def ip_authorized?(request, ipmasks)
77
79
  return true if ipmasks.nil?
78
80
 
79
81
  ipmasks.any? do |ip_mask|
80
- ip_mask.include?(IPAddr.new(@original_request.ip))
82
+ ip_mask.include?(IPAddr.new(request.ip))
81
83
  end
82
84
  end
83
85
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Backstage
3
5
  File = ::File
@@ -8,10 +10,10 @@ module Rack
8
10
  end
9
11
 
10
12
  def call(env)
11
- if File.exists?(@file)
13
+ if File.exist?(@file)
12
14
  content = File.read(@file)
13
- length = "".respond_to?(:bytesize) ? content.bytesize.to_s : content.size.to_s
14
- [503, {'Content-Type' => 'text/html', 'Content-Length' => length}, [content]]
15
+ length = content.bytesize.to_s
16
+ [503, {'content-type' => 'text/html', 'content-length' => length}, [content]]
15
17
  else
16
18
  @app.call(env)
17
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Bounce those annoying favicon.ico requests
3
5
  class BounceFavicon
@@ -7,7 +9,7 @@ module Rack
7
9
 
8
10
  def call(env)
9
11
  if env["PATH_INFO"] == "/favicon.ico"
10
- [404, {"Content-Type" => "text/html", "Content-Length" => "0"}, []]
12
+ [404, {"content-type" => "text/html", "content-length" => "0"}, []]
11
13
  else
12
14
  @app.call(env)
13
15
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Callbacks
3
5
  def initialize(&block)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack middleware to use common cookies across domain and subdomains.
3
5
  class CommonCookies
@@ -5,26 +7,32 @@ module Rack
5
7
  LOCALHOST_OR_IP_REGEXP = /^([\d.]+|localhost)$/
6
8
  PORT = /:\d+$/
7
9
 
10
+ HEADERS_KLASS = Rack.release < "3" ? Utils::HeaderHash : Headers
11
+ private_constant :HEADERS_KLASS
12
+
8
13
  def initialize(app)
9
14
  @app = app
10
15
  end
11
16
 
12
17
  def call(env)
13
- @app.call(env).tap do |(status, headers, response)|
14
- @host = env['HTTP_HOST'].sub PORT, ''
15
- share_cookie headers
16
- end
18
+ status, headers, body = @app.call(env)
19
+ headers = HEADERS_KLASS.new.merge(headers)
20
+
21
+ host = env['HTTP_HOST'].sub PORT, ''
22
+ share_cookie(headers, host)
23
+
24
+ [status, headers, body]
17
25
  end
18
26
 
19
27
  private
20
28
 
21
- def domain
22
- @host =~ DOMAIN_REGEXP
29
+ def domain(host)
30
+ host =~ DOMAIN_REGEXP
23
31
  ".#{$1}.#{$2}"
24
32
  end
25
33
 
26
- def share_cookie(headers)
27
- headers['Set-Cookie'] &&= common_cookie(headers) if @host !~ LOCALHOST_OR_IP_REGEXP
34
+ def share_cookie(headers, host)
35
+ headers['Set-Cookie'] &&= common_cookie(headers, host) if host !~ LOCALHOST_OR_IP_REGEXP
28
36
  end
29
37
 
30
38
  def cookie(headers)
@@ -32,8 +40,8 @@ module Rack
32
40
  cookies.is_a?(Array) ? cookies.join("\n") : cookies
33
41
  end
34
42
 
35
- def common_cookie(headers)
36
- cookie(headers).gsub(/; domain=[^;]*/, '').gsub(/$/, "; domain=#{domain}")
43
+ def common_cookie(headers, host)
44
+ cookie(headers).gsub(/; domain=[^;]*/, '').gsub(/$/, "; domain=#{domain(host)}")
37
45
  end
38
46
  end
39
- end
47
+ end
@@ -1,16 +1,4 @@
1
- module Rack
1
+ # frozen_string_literal: true
2
2
 
3
- # Rack::Config modifies the environment using the block given during
4
- # initialization.
5
- class Config
6
- def initialize(app, &block)
7
- @app = app
8
- @block = block
9
- end
10
-
11
- def call(env)
12
- @block.call(env)
13
- @app.call(env)
14
- end
15
- end
16
- end
3
+ require 'rack'
4
+ require 'rack/config'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Cookies
3
5
  class CookieJar < Hash
@@ -1,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'csshttprequest'
2
4
 
3
5
  module Rack
4
6
 
5
7
  # A Rack middleware for providing CSSHTTPRequest responses.
6
8
  class CSSHTTPRequest
9
+ HEADERS_KLASS = Rack.release < "3" ? Utils::HeaderHash : Headers
10
+ private_constant :HEADERS_KLASS
7
11
 
8
12
  def initialize(app)
9
13
  @app = app
@@ -13,9 +17,12 @@ module Rack
13
17
  # the CSSHTTPRequest encoder
14
18
  def call(env)
15
19
  status, headers, response = @app.call(env)
20
+ headers = HEADERS_KLASS.new.merge(headers)
21
+
16
22
  if chr_request?(env)
17
- response = encode(response)
18
- modify_headers!(headers, response)
23
+ encoded_response = encode(response)
24
+ modify_headers!(headers, encoded_response)
25
+ response = [encoded_response]
19
26
  end
20
27
  [status, headers, response]
21
28
  end
@@ -25,13 +32,12 @@ module Rack
25
32
  !(/\.chr$/.match(env['PATH_INFO'])).nil? || Rack::Request.new(env).params['_format'] == 'chr'
26
33
  end
27
34
 
28
- def encode(response, assembled_body="")
29
- response.each { |s| assembled_body << s.to_s } # call down the stack
30
- return ::CSSHTTPRequest.encode(assembled_body)
35
+ def encode(body)
36
+ ::CSSHTTPRequest.encode(body.to_enum.to_a.join)
31
37
  end
32
38
 
33
39
  def modify_headers!(headers, encoded_response)
34
- headers['Content-Length'] = encoded_response.length.to_s
40
+ headers['Content-Length'] = encoded_response.bytesize.to_s
35
41
  headers['Content-Type'] = 'text/css'
36
42
  nil
37
43
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  # TODO: optional stats
@@ -63,14 +65,14 @@ module Rack
63
65
  end
64
66
 
65
67
  def deflect!
66
- [403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, []]
68
+ [403, { 'content-type' => 'text/html', 'content-length' => '0' }, []]
67
69
  end
68
70
 
69
71
  def deflect? env
70
- @remote_addr = env['REMOTE_ADDR']
71
- return false if options[:whitelist].include? @remote_addr
72
- return true if options[:blacklist].include? @remote_addr
73
- sync { watch }
72
+ remote_addr = env['REMOTE_ADDR']
73
+ return false if options[:whitelist].include? remote_addr
74
+ return true if options[:blacklist].include? remote_addr
75
+ sync { watch(remote_addr) }
74
76
  end
75
77
 
76
78
  def log message
@@ -82,55 +84,55 @@ module Rack
82
84
  @mutex.synchronize(&block)
83
85
  end
84
86
 
85
- def map
86
- @remote_addr_map[@remote_addr] ||= {
87
+ def map(remote_addr)
88
+ @remote_addr_map[remote_addr] ||= {
87
89
  :expires => Time.now + options[:interval],
88
90
  :requests => 0
89
91
  }
90
92
  end
91
93
 
92
- def watch
93
- increment_requests
94
- clear! if watch_expired? and not blocked?
95
- clear! if blocked? and block_expired?
96
- block! if watching? and exceeded_request_threshold?
97
- blocked?
94
+ def watch(remote_addr)
95
+ increment_requests(remote_addr)
96
+ clear!(remote_addr) if watch_expired?(remote_addr) and not blocked?(remote_addr)
97
+ clear!(remote_addr) if blocked?(remote_addr) and block_expired?(remote_addr)
98
+ block!(remote_addr) if watching?(remote_addr) and exceeded_request_threshold?(remote_addr)
99
+ blocked?(remote_addr)
98
100
  end
99
101
 
100
- def block!
101
- return if blocked?
102
- log "blocked #{@remote_addr}"
103
- map[:block_expires] = Time.now + options[:block_duration]
102
+ def block!(remote_addr)
103
+ return if blocked?(remote_addr)
104
+ log "blocked #{remote_addr}"
105
+ map(remote_addr)[:block_expires] = Time.now + options[:block_duration]
104
106
  end
105
107
 
106
- def blocked?
107
- map.has_key? :block_expires
108
+ def blocked?(remote_addr)
109
+ map(remote_addr).has_key? :block_expires
108
110
  end
109
111
 
110
- def block_expired?
111
- map[:block_expires] < Time.now rescue false
112
+ def block_expired?(remote_addr)
113
+ map(remote_addr)[:block_expires] < Time.now rescue false
112
114
  end
113
115
 
114
- def watching?
115
- @remote_addr_map.has_key? @remote_addr
116
+ def watching?(remote_addr)
117
+ @remote_addr_map.has_key? remote_addr
116
118
  end
117
119
 
118
- def clear!
119
- return unless watching?
120
- log "released #{@remote_addr}" if blocked?
121
- @remote_addr_map.delete @remote_addr
120
+ def clear!(remote_addr)
121
+ return unless watching?(remote_addr)
122
+ log "released #{remote_addr}" if blocked?(remote_addr)
123
+ @remote_addr_map.delete remote_addr
122
124
  end
123
125
 
124
- def increment_requests
125
- map[:requests] += 1
126
+ def increment_requests(remote_addr)
127
+ map(remote_addr)[:requests] += 1
126
128
  end
127
129
 
128
- def exceeded_request_threshold?
129
- map[:requests] > options[:request_threshold]
130
+ def exceeded_request_threshold?(remote_addr)
131
+ map(remote_addr)[:requests] > options[:request_threshold]
130
132
  end
131
133
 
132
- def watch_expired?
133
- map[:expires] <= Time.now
134
+ def watch_expired?(remote_addr)
135
+ map(remote_addr)[:expires] <= Time.now
134
136
  end
135
137
 
136
138
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Ensure that the path and query string presented to the application
3
5
  # contains only valid characters. If the validation fails, then a
@@ -16,7 +18,7 @@ module Rack
16
18
  Rack::Utils.unescape(full_path).valid_encoding?
17
19
  @app.call env
18
20
  else
19
- [400, {'Content-Type'=>'text/plain'}, ['Bad Request']]
21
+ [400, {'content-type'=>'text/plain'}, ['Bad Request']]
20
22
  end
21
23
  end
22
24
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Evil
3
5
  # Lets you return a response to the client immediately from anywhere ( M V or C ) in the code.
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ExpectationCascade
3
- Expect = "Expect".freeze
5
+ Expect = "HTTP_EXPECT".freeze
4
6
  ContinueExpectation = "100-continue".freeze
5
7
 
6
- ExpectationFailed = [417, {"Content-Type" => "text/html"}, []].freeze
7
- NotFound = [404, {"Content-Type" => "text/html"}, []].freeze
8
+ ExpectationFailed = [417, {"content-type" => "text/html"}, []]
9
+ NotFound = [404, {"content-type" => "text/html"}, []]
8
10
 
9
11
  attr_reader :apps
10
12
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Forces garbage collection after each request.
3
5
  class GarbageCollector
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
 
3
5
  # Rack middleware implementing the IETF draft: "Host Metadata for the Web"
@@ -28,7 +30,7 @@ module Rack
28
30
 
29
31
  def call(env)
30
32
  if env['PATH_INFO'] == '/host-meta'
31
- [200, {'Content-Type' => 'application/host-meta'}, [@response]]
33
+ [200, {'content-type' => 'application/host-meta'}, [@response]]
32
34
  else
33
35
  @app.call(env)
34
36
  end
@@ -0,0 +1,94 @@
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
+ begin
59
+ if @verbs.include?(env[Rack::REQUEST_METHOD]) &&
60
+ @matcher.call(@media, env['CONTENT_TYPE'])
61
+
62
+ update_form_hash_with_json_body(env)
63
+ end
64
+ rescue ParserError
65
+ body = { error: 'Failed to parse body as JSON' }.to_json
66
+ header = { 'content-type' => 'application/json' }
67
+ return Rack::Response.new(body, 400, header).finish
68
+ end
69
+ @app.call(env)
70
+ end
71
+
72
+ private
73
+
74
+ class ParserError < StandardError; end
75
+
76
+ def update_form_hash_with_json_body(env)
77
+ body = env[Rack::RACK_INPUT]
78
+ return unless (body_content = body.read) && !body_content.empty?
79
+
80
+ body.rewind if body.respond_to?(:rewind) # somebody might try to read this stream
81
+
82
+ begin
83
+ parsed_body = @parser.call(body_content)
84
+ rescue StandardError
85
+ raise ParserError
86
+ end
87
+
88
+ env.update(
89
+ Rack::RACK_REQUEST_FORM_HASH => parsed_body,
90
+ Rack::RACK_REQUEST_FORM_INPUT => body
91
+ )
92
+ end
93
+ end
94
+ end
@@ -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.
@@ -21,6 +23,9 @@ module Rack
21
23
  # "\342\200\251" # => "\u2029"
22
24
  U2028, U2029 = ("\u2028" == 'u2028') ? ["\342\200\250", "\342\200\251"] : ["\u2028", "\u2029"]
23
25
 
26
+ HEADERS_KLASS = Rack.release < "3" ? Utils::HeaderHash : Headers
27
+ private_constant :HEADERS_KLASS
28
+
24
29
  def initialize(app)
25
30
  @app = app
26
31
  end
@@ -40,7 +45,7 @@ module Rack
40
45
  return status, headers, response
41
46
  end
42
47
 
43
- headers = HeaderHash.new(headers)
48
+ headers = HEADERS_KLASS.new.merge(headers)
44
49
 
45
50
  if is_json?(headers) && has_callback?(request)
46
51
  callback = request.params['callback']
@@ -53,7 +58,7 @@ module Rack
53
58
 
54
59
  # Set new Content-Length, if it was set before we mutated the response body
55
60
  if headers['Content-Length']
56
- length = response.to_ary.inject(0) { |len, part| len + part.bytesize }
61
+ length = response.map(&:bytesize).reduce(0, :+)
57
62
  headers['Content-Length'] = length.to_s
58
63
  end
59
64
  end
@@ -87,8 +92,8 @@ module Rack
87
92
  # method of combining all of the data into a single string makes sense
88
93
  # since JSON is returned as a full string.
89
94
  #
90
- def pad(callback, response, body = "")
91
- response.each do |s|
95
+ def pad(callback, response)
96
+ body = response.to_enum.map do |s|
92
97
  # U+2028 and U+2029 are allowed inside strings in JSON (as all literal
93
98
  # Unicode characters) but JavaScript defines them as newline
94
99
  # seperators. Because no literal newlines are allowed in a string, this
@@ -96,8 +101,8 @@ module Rack
96
101
  # replacing them with the escaped version. This should be safe because
97
102
  # according to the JSON spec, these characters are *only* valid inside
98
103
  # a string and should therefore not be present any other places.
99
- body << s.to_s.gsub(U2028, '\u2028').gsub(U2029, '\u2029')
100
- end
104
+ s.gsub(U2028, '\u2028').gsub(U2029, '\u2029')
105
+ end.join
101
106
 
102
107
  # https://github.com/rack/rack-contrib/issues/46
103
108
  response.close if response.respond_to?(:close)
@@ -106,7 +111,7 @@ module Rack
106
111
  end
107
112
 
108
113
  def bad_request(body = "Bad Request")
109
- [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body.size.to_s }, [body] ]
114
+ [ 400, { 'content-type' => 'text/plain', 'content-length' => body.bytesize.to_s }, [body] ]
110
115
  end
111
116
 
112
117
  end