rack 2.1.3 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +613 -1
  3. data/CONTRIBUTING.md +136 -0
  4. data/README.rdoc +83 -39
  5. data/Rakefile +14 -7
  6. data/{SPEC → SPEC.rdoc} +26 -1
  7. data/lib/rack.rb +7 -16
  8. data/lib/rack/auth/abstract/request.rb +0 -2
  9. data/lib/rack/auth/basic.rb +3 -3
  10. data/lib/rack/auth/digest/md5.rb +4 -4
  11. data/lib/rack/auth/digest/request.rb +3 -3
  12. data/lib/rack/body_proxy.rb +13 -9
  13. data/lib/rack/builder.rb +77 -8
  14. data/lib/rack/cascade.rb +23 -8
  15. data/lib/rack/chunked.rb +48 -23
  16. data/lib/rack/common_logger.rb +25 -18
  17. data/lib/rack/conditional_get.rb +18 -16
  18. data/lib/rack/content_length.rb +6 -7
  19. data/lib/rack/content_type.rb +3 -4
  20. data/lib/rack/deflater.rb +45 -35
  21. data/lib/rack/directory.rb +77 -59
  22. data/lib/rack/etag.rb +2 -3
  23. data/lib/rack/events.rb +15 -18
  24. data/lib/rack/file.rb +1 -1
  25. data/lib/rack/files.rb +96 -56
  26. data/lib/rack/handler/cgi.rb +1 -4
  27. data/lib/rack/handler/fastcgi.rb +1 -3
  28. data/lib/rack/handler/lsws.rb +1 -3
  29. data/lib/rack/handler/scgi.rb +1 -3
  30. data/lib/rack/handler/thin.rb +15 -11
  31. data/lib/rack/handler/webrick.rb +12 -5
  32. data/lib/rack/head.rb +0 -2
  33. data/lib/rack/lint.rb +57 -14
  34. data/lib/rack/lobster.rb +3 -5
  35. data/lib/rack/lock.rb +0 -1
  36. data/lib/rack/mock.rb +22 -4
  37. data/lib/rack/multipart.rb +1 -1
  38. data/lib/rack/multipart/generator.rb +11 -6
  39. data/lib/rack/multipart/parser.rb +7 -15
  40. data/lib/rack/multipart/uploaded_file.rb +13 -7
  41. data/lib/rack/query_parser.rb +7 -8
  42. data/lib/rack/recursive.rb +1 -1
  43. data/lib/rack/reloader.rb +1 -3
  44. data/lib/rack/request.rb +182 -76
  45. data/lib/rack/response.rb +62 -19
  46. data/lib/rack/rewindable_input.rb +0 -1
  47. data/lib/rack/runtime.rb +3 -3
  48. data/lib/rack/sendfile.rb +0 -3
  49. data/lib/rack/server.rb +9 -8
  50. data/lib/rack/session/abstract/id.rb +20 -18
  51. data/lib/rack/session/cookie.rb +2 -3
  52. data/lib/rack/session/pool.rb +1 -1
  53. data/lib/rack/show_exceptions.rb +2 -4
  54. data/lib/rack/show_status.rb +1 -3
  55. data/lib/rack/static.rb +13 -6
  56. data/lib/rack/tempfile_reaper.rb +0 -2
  57. data/lib/rack/urlmap.rb +1 -4
  58. data/lib/rack/utils.rb +58 -54
  59. data/lib/rack/version.rb +29 -0
  60. data/rack.gemspec +31 -29
  61. metadata +11 -12
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'core_ext/regexp'
4
-
5
3
  module Rack
6
4
  class QueryParser
7
- using ::Rack::RegexpExtensions
5
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
8
6
 
9
7
  DEFAULT_SEP = /[&;] */n
10
8
  COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
@@ -64,18 +62,19 @@ module Rack
64
62
  # ParameterTypeError is raised. Users are encouraged to return a 400 in this
65
63
  # case.
66
64
  def parse_nested_query(qs, d = nil)
67
- return {} if qs.nil? || qs.empty?
68
65
  params = make_params
69
66
 
70
- qs.split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
71
- k, v = p.split('=', 2).map! { |s| unescape(s) }
67
+ unless qs.nil? || qs.empty?
68
+ (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
69
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
72
70
 
73
- normalize_params(params, k, v, param_depth_limit)
71
+ normalize_params(params, k, v, param_depth_limit)
72
+ end
74
73
  end
75
74
 
76
75
  return params.to_h
77
76
  rescue ArgumentError => e
78
- raise InvalidParameterError, e.message
77
+ raise InvalidParameterError, e.message, e.backtrace
79
78
  end
80
79
 
81
80
  # normalize_params recursively expands parameters into structural types. If
@@ -19,7 +19,7 @@ module Rack
19
19
  @env[PATH_INFO] = @url.path
20
20
  @env[QUERY_STRING] = @url.query if @url.query
21
21
  @env[HTTP_HOST] = @url.host if @url.host
22
- @env["HTTP_PORT"] = @url.port if @url.port
22
+ @env[HTTP_PORT] = @url.port if @url.port
23
23
  @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
24
24
 
25
25
  super "forwarding to #{url}"
@@ -6,8 +6,6 @@
6
6
 
7
7
  require 'pathname'
8
8
 
9
- require_relative 'core_ext/regexp'
10
-
11
9
  module Rack
12
10
 
13
11
  # High performant source reloader
@@ -24,7 +22,7 @@ module Rack
24
22
  # It is performing a check/reload cycle at the start of every request, but
25
23
  # also respects a cool down time, during which nothing will be done.
26
24
  class Reloader
27
- using ::Rack::RegexpExtensions
25
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
28
26
 
29
27
  def initialize(app, cooldown = 10, backend = Stat)
30
28
  @app = app
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
4
- require 'rack/media_type'
5
-
6
- require_relative 'core_ext/regexp'
7
-
8
3
  module Rack
9
4
  # Rack::Request provides a convenient interface to a Rack
10
5
  # environment. It is stateless, the environment +env+ passed to the
@@ -15,7 +10,7 @@ module Rack
15
10
  # req.params["data"]
16
11
 
17
12
  class Request
18
- using ::Rack::RegexpExtensions
13
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
19
14
 
20
15
  class << self
21
16
  attr_accessor :ip_filter
@@ -93,7 +88,7 @@ module Rack
93
88
  # assert_equal 'image/png,*/*', request.get_header('Accept')
94
89
  #
95
90
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
96
- def add_header key, v
91
+ def add_header(key, v)
97
92
  if v.nil?
98
93
  get_header key
99
94
  elsif has_header? key
@@ -134,11 +129,23 @@ module Rack
134
129
  # to include the port in a generated URI.
135
130
  DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
136
131
 
132
+ # The address of the client which connected to the proxy.
133
+ HTTP_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'
134
+
135
+ # The contents of the host/:authority header sent to the proxy.
136
+ HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
137
+
138
+ # The value of the scheme sent to the proxy.
137
139
  HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
138
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
139
- HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
140
- HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
141
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
140
+
141
+ # The protocol used to connect to the proxy.
142
+ HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
143
+
144
+ # The port used to connect to the proxy.
145
+ HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
146
+
147
+ # Another way for specifing https scheme was used.
148
+ HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
142
149
 
143
150
  def body; get_header(RACK_INPUT) end
144
151
  def script_name; get_header(SCRIPT_NAME).to_s end
@@ -212,19 +219,52 @@ module Rack
212
219
  end
213
220
  end
214
221
 
222
+ # The authority of the incoming request as defined by RFC3976.
223
+ # https://tools.ietf.org/html/rfc3986#section-3.2
224
+ #
225
+ # In HTTP/1, this is the `host` header.
226
+ # In HTTP/2, this is the `:authority` pseudo-header.
215
227
  def authority
216
- get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT)
228
+ forwarded_authority || host_authority || server_authority
229
+ end
230
+
231
+ # The authority as defined by the `SERVER_NAME` and `SERVER_PORT`
232
+ # variables.
233
+ def server_authority
234
+ host = self.server_name
235
+ port = self.server_port
236
+
237
+ if host
238
+ if port
239
+ "#{host}:#{port}"
240
+ else
241
+ host
242
+ end
243
+ end
244
+ end
245
+
246
+ def server_name
247
+ get_header(SERVER_NAME)
248
+ end
249
+
250
+ def server_port
251
+ if port = get_header(SERVER_PORT)
252
+ Integer(port)
253
+ end
217
254
  end
218
255
 
219
256
  def cookies
220
- hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
221
- set_header(k, {})
257
+ hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |key|
258
+ set_header(key, {})
259
+ end
260
+
261
+ string = get_header(HTTP_COOKIE)
262
+
263
+ unless string == get_header(RACK_REQUEST_COOKIE_STRING)
264
+ hash.replace Utils.parse_cookies_header(string)
265
+ set_header(RACK_REQUEST_COOKIE_STRING, string)
222
266
  end
223
- string = get_header HTTP_COOKIE
224
267
 
225
- return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
226
- hash.replace Utils.parse_cookies_header string
227
- set_header(RACK_REQUEST_COOKIE_STRING, string)
228
268
  hash
229
269
  end
230
270
 
@@ -237,52 +277,101 @@ module Rack
237
277
  get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
238
278
  end
239
279
 
240
- def host_with_port
241
- if forwarded = get_header(HTTP_X_FORWARDED_HOST)
242
- forwarded.split(/,\s?/).last
280
+ # The `HTTP_HOST` header.
281
+ def host_authority
282
+ get_header(HTTP_HOST)
283
+ end
284
+
285
+ def host_with_port(authority = self.authority)
286
+ host, _, port = split_authority(authority)
287
+
288
+ if port == DEFAULT_PORTS[self.scheme]
289
+ host
243
290
  else
244
- get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}"
291
+ authority
245
292
  end
246
293
  end
247
294
 
295
+ # Returns a formatted host, suitable for being used in a URI.
248
296
  def host
249
- # Remove port number.
250
- h = host_with_port
251
- if colon_index = h.index(":")
252
- h[0, colon_index]
253
- else
254
- h
255
- end
297
+ split_authority(self.authority)[0]
298
+ end
299
+
300
+ # Returns an address suitable for being to resolve to an address.
301
+ # In the case of a domain name or IPv4 address, the result is the same
302
+ # as +host+. In the case of IPv6 or future address formats, the square
303
+ # brackets are removed.
304
+ def hostname
305
+ split_authority(self.authority)[1]
256
306
  end
257
307
 
258
308
  def port
259
- if port = extract_port(host_with_port)
260
- port.to_i
261
- elsif port = get_header(HTTP_X_FORWARDED_PORT)
262
- port.to_i
263
- elsif has_header?(HTTP_X_FORWARDED_HOST)
264
- DEFAULT_PORTS[scheme]
265
- elsif has_header?(HTTP_X_FORWARDED_PROTO)
266
- DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))]
267
- else
268
- get_header(SERVER_PORT).to_i
309
+ if authority = self.authority
310
+ _, _, port = split_authority(self.authority)
311
+
312
+ if port
313
+ return port
314
+ end
315
+ end
316
+
317
+ if forwarded_port = self.forwarded_port
318
+ return forwarded_port.first
319
+ end
320
+
321
+ if scheme = self.scheme
322
+ if port = DEFAULT_PORTS[self.scheme]
323
+ return port
324
+ end
325
+ end
326
+
327
+ self.server_port
328
+ end
329
+
330
+ def forwarded_for
331
+ if value = get_header(HTTP_X_FORWARDED_FOR)
332
+ split_header(value).map do |authority|
333
+ split_authority(wrap_ipv6(authority))[1]
334
+ end
335
+ end
336
+ end
337
+
338
+ def forwarded_port
339
+ if value = get_header(HTTP_X_FORWARDED_PORT)
340
+ split_header(value).map(&:to_i)
341
+ end
342
+ end
343
+
344
+ def forwarded_authority
345
+ if value = get_header(HTTP_X_FORWARDED_HOST)
346
+ wrap_ipv6(split_header(value).first)
269
347
  end
270
348
  end
271
349
 
272
350
  def ssl?
273
- scheme == 'https'
351
+ scheme == 'https' || scheme == 'wss'
274
352
  end
275
353
 
276
354
  def ip
277
- remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
278
- remote_addrs = reject_trusted_ip_addresses(remote_addrs)
355
+ remote_addresses = split_header(get_header('REMOTE_ADDR'))
356
+ external_addresses = reject_trusted_ip_addresses(remote_addresses)
279
357
 
280
- return remote_addrs.first if remote_addrs.any?
358
+ unless external_addresses.empty?
359
+ return external_addresses.first
360
+ end
281
361
 
282
- forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
283
- .map { |ip| strip_port(ip) }
362
+ if forwarded_for = self.forwarded_for
363
+ unless forwarded_for.empty?
364
+ # The forwarded for addresses are ordered: client, proxy1, proxy2.
365
+ # So we reject all the trusted addresses (proxy*) and return the
366
+ # last client. Or if we trust everyone, we just return the first
367
+ # address.
368
+ return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
369
+ end
370
+ end
284
371
 
285
- return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
372
+ # If all the addresses are trusted, and we aren't forwarded, just return
373
+ # the first remote address, which represents the source of the request.
374
+ remote_addresses.first
286
375
  end
287
376
 
288
377
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -323,6 +412,7 @@ module Rack
323
412
  def form_data?
324
413
  type = media_type
325
414
  meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
415
+
326
416
  (meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
327
417
  end
328
418
 
@@ -377,8 +467,6 @@ module Rack
377
467
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
378
468
  def params
379
469
  self.GET.merge(self.POST)
380
- rescue EOFError
381
- self.GET.dup
382
470
  end
383
471
 
384
472
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
@@ -412,9 +500,7 @@ module Rack
412
500
  end
413
501
 
414
502
  def base_url
415
- url = "#{scheme}://#{host}"
416
- url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
417
- url
503
+ "#{scheme}://#{host_with_port}"
418
504
  end
419
505
 
420
506
  # Tries to return a remake of the original request URL as a string.
@@ -471,6 +557,20 @@ module Rack
471
557
 
472
558
  def default_session; {}; end
473
559
 
560
+ # Assist with compatibility when processing `X-Forwarded-For`.
561
+ def wrap_ipv6(host)
562
+ # Even thought IPv6 addresses should be wrapped in square brackets,
563
+ # sometimes this is not done in various legacy/underspecified headers.
564
+ # So we try to fix this situation for compatibility reasons.
565
+
566
+ # Try to detect IPv6 addresses which aren't escaped yet:
567
+ if !host.start_with?('[') && host.count(':') > 1
568
+ "[#{host}]"
569
+ else
570
+ host
571
+ end
572
+ end
573
+
474
574
  def parse_http_accept_header(header)
475
575
  header.to_s.split(/\s*,\s*/).map do |part|
476
576
  attribute, parameters = part.split(/\s*;\s*/, 2)
@@ -494,27 +594,39 @@ module Rack
494
594
  Rack::Multipart.extract_multipart(self, query_parser)
495
595
  end
496
596
 
497
- def split_ip_addresses(ip_addresses)
498
- ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
499
- end
500
-
501
- def strip_port(ip_address)
502
- # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
503
- # returns: "2001:db8:cafe::17"
504
- sep_start = ip_address.index('[')
505
- sep_end = ip_address.index(']')
506
- if (sep_start && sep_end)
507
- return ip_address[sep_start + 1, sep_end - 1]
508
- end
509
-
510
- # IPv4 format with optional port: "192.0.2.43:47011"
511
- # returns: "192.0.2.43"
512
- sep = ip_address.index(':')
513
- if (sep && ip_address.count(':') == 1)
514
- return ip_address[0, sep]
597
+ def split_header(value)
598
+ value ? value.strip.split(/[,\s]+/) : []
599
+ end
600
+
601
+ AUTHORITY = /
602
+ # The host:
603
+ (?<host>
604
+ # An IPv6 address:
605
+ (\[(?<ip6>.*)\])
606
+ |
607
+ # An IPv4 address:
608
+ (?<ip4>[\d\.]+)
609
+ |
610
+ # A hostname:
611
+ (?<name>[a-zA-Z0-9\.\-]+)
612
+ )
613
+ # The optional port:
614
+ (:(?<port>\d+))?
615
+ /x
616
+
617
+ private_constant :AUTHORITY
618
+
619
+ def split_authority(authority)
620
+ if match = AUTHORITY.match(authority)
621
+ if address = match[:ip6]
622
+ return match[:host], address, match[:port]&.to_i
623
+ else
624
+ return match[:host], match[:host], match[:port]&.to_i
625
+ end
515
626
  end
516
627
 
517
- ip_address
628
+ # Give up!
629
+ return authority, authority, nil
518
630
  end
519
631
 
520
632
  def reject_trusted_ip_addresses(ip_addresses)
@@ -539,12 +651,6 @@ module Rack
539
651
  end
540
652
  end
541
653
  end
542
-
543
- def extract_port(uri)
544
- if (colon_index = uri.index(':'))
545
- uri[colon_index + 1, uri.length]
546
- end
547
- end
548
654
  end
549
655
 
550
656
  include Env
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/request'
4
- require 'rack/utils'
5
- require 'rack/body_proxy'
6
- require 'rack/media_type'
7
3
  require 'time'
8
4
 
9
5
  module Rack
@@ -19,34 +15,51 @@ module Rack
19
15
  # +write+ are synchronous with the Rack response.
20
16
  #
21
17
  # Your application's +call+ should end returning Response#finish.
22
-
23
18
  class Response
24
- attr_accessor :length, :status, :body
25
- attr_reader :header
26
- alias headers header
19
+ def self.[](status, headers, body)
20
+ self.new(body, status, headers)
21
+ end
27
22
 
28
23
  CHUNKED = 'chunked'
29
24
  STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
30
25
 
31
- def initialize(body = nil, status = 200, header = {})
26
+ attr_accessor :length, :status, :body
27
+ attr_reader :headers
28
+
29
+ # @deprecated Use {#headers} instead.
30
+ alias header headers
31
+
32
+ # Initialize the response object with the specified body, status
33
+ # and headers.
34
+ #
35
+ # @param body [nil, #each, #to_str] the response body.
36
+ # @param status [Integer] the integer status as defined by the
37
+ # HTTP protocol RFCs.
38
+ # @param headers [#each] a list of key-value header pairs which
39
+ # conform to the HTTP protocol RFCs.
40
+ #
41
+ # Providing a body which responds to #to_str is legacy behaviour.
42
+ def initialize(body = nil, status = 200, headers = {})
32
43
  @status = status.to_i
33
- @header = Utils::HeaderHash.new(header)
44
+ @headers = Utils::HeaderHash[headers]
34
45
 
35
46
  @writer = self.method(:append)
36
47
 
37
48
  @block = nil
38
- @length = 0
39
49
 
40
50
  # Keep track of whether we have expanded the user supplied body.
41
51
  if body.nil?
42
52
  @body = []
43
53
  @buffered = true
54
+ @length = 0
44
55
  elsif body.respond_to?(:to_str)
45
56
  @body = [body]
46
57
  @buffered = true
58
+ @length = body.to_str.bytesize
47
59
  else
48
60
  @body = body
49
61
  @buffered = false
62
+ @length = 0
50
63
  end
51
64
 
52
65
  yield self if block_given?
@@ -61,18 +74,21 @@ module Rack
61
74
  CHUNKED == get_header(TRANSFER_ENCODING)
62
75
  end
63
76
 
77
+ # Generate a response array consistent with the requirements of the SPEC.
78
+ # @return [Array] a 3-tuple suitable of `[status, headers, body]`
79
+ # which is suitable to be returned from the middleware `#call(env)` method.
64
80
  def finish(&block)
65
81
  if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
66
82
  delete_header CONTENT_TYPE
67
83
  delete_header CONTENT_LENGTH
68
84
  close
69
- [status.to_i, header, []]
85
+ return [@status, @headers, []]
70
86
  else
71
87
  if block_given?
72
88
  @block = block
73
- [status.to_i, header, self]
89
+ return [@status, @headers, self]
74
90
  else
75
- [status.to_i, header, @body]
91
+ return [@status, @headers, @body]
76
92
  end
77
93
  end
78
94
  end
@@ -152,7 +168,7 @@ module Rack
152
168
  # assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
153
169
  #
154
170
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
155
- def add_header key, v
171
+ def add_header(key, v)
156
172
  if v.nil?
157
173
  get_header key
158
174
  elsif has_header? key
@@ -162,10 +178,16 @@ module Rack
162
178
  end
163
179
  end
164
180
 
181
+ # Get the content type of the response.
165
182
  def content_type
166
183
  get_header CONTENT_TYPE
167
184
  end
168
185
 
186
+ # Set the content type of the response.
187
+ def content_type=(content_type)
188
+ set_header CONTENT_TYPE, content_type
189
+ end
190
+
169
191
  def media_type
170
192
  MediaType.type(content_type)
171
193
  end
@@ -200,7 +222,7 @@ module Rack
200
222
  get_header SET_COOKIE
201
223
  end
202
224
 
203
- def set_cookie_header= v
225
+ def set_cookie_header=(v)
204
226
  set_header SET_COOKIE, v
205
227
  end
206
228
 
@@ -208,15 +230,31 @@ module Rack
208
230
  get_header CACHE_CONTROL
209
231
  end
210
232
 
211
- def cache_control= v
233
+ def cache_control=(v)
212
234
  set_header CACHE_CONTROL, v
213
235
  end
214
236
 
237
+ # Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
238
+ def do_not_cache!
239
+ set_header CACHE_CONTROL, "no-cache, must-revalidate"
240
+ set_header EXPIRES, Time.now.httpdate
241
+ end
242
+
243
+ # Specify that the content should be cached.
244
+ # @param duration [Integer] The number of seconds until the cache expires.
245
+ # @option directive [String] The cache control directive, one of "public", "private", "no-cache" or "no-store".
246
+ def cache!(duration = 3600, directive: "public")
247
+ unless headers[CACHE_CONTROL] =~ /no-cache/
248
+ set_header CACHE_CONTROL, "#{directive}, max-age=#{duration}"
249
+ set_header EXPIRES, (Time.now + duration).httpdate
250
+ end
251
+ end
252
+
215
253
  def etag
216
254
  get_header ETAG
217
255
  end
218
256
 
219
- def etag= v
257
+ def etag=(v)
220
258
  set_header ETAG, v
221
259
  end
222
260
 
@@ -228,6 +266,9 @@ module Rack
228
266
  if @body.is_a?(Array)
229
267
  # The user supplied body was an array:
230
268
  @body = @body.compact
269
+ @body.each do |part|
270
+ @length += part.to_s.bytesize
271
+ end
231
272
  else
232
273
  # Turn the user supplied body into a buffered array:
233
274
  body = @body
@@ -236,6 +277,8 @@ module Rack
236
277
  body.each do |part|
237
278
  @writer.call(part.to_s)
238
279
  end
280
+
281
+ body.close if body.respond_to?(:close)
239
282
  end
240
283
 
241
284
  @buffered = true
@@ -261,7 +304,7 @@ module Rack
261
304
  attr_reader :headers
262
305
  attr_accessor :status
263
306
 
264
- def initialize status, headers
307
+ def initialize(status, headers)
265
308
  @status = status
266
309
  @headers = headers
267
310
  end