rack 2.0.4 → 2.1.0

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/{HISTORY.md → CHANGELOG.md} +220 -155
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +77 -119
  5. data/Rakefile +25 -18
  6. data/SPEC +3 -4
  7. data/bin/rackup +1 -0
  8. data/example/lobster.ru +2 -0
  9. data/example/protectedlobster.rb +3 -1
  10. data/example/protectedlobster.ru +2 -0
  11. data/lib/rack.rb +63 -60
  12. data/lib/rack/auth/abstract/handler.rb +3 -1
  13. data/lib/rack/auth/abstract/request.rb +2 -0
  14. data/lib/rack/auth/basic.rb +4 -1
  15. data/lib/rack/auth/digest/md5.rb +9 -7
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +4 -2
  18. data/lib/rack/auth/digest/request.rb +2 -0
  19. data/lib/rack/body_proxy.rb +3 -6
  20. data/lib/rack/builder.rb +38 -15
  21. data/lib/rack/cascade.rb +6 -5
  22. data/lib/rack/chunked.rb +29 -6
  23. data/lib/rack/common_logger.rb +9 -8
  24. data/lib/rack/conditional_get.rb +3 -1
  25. data/lib/rack/config.rb +2 -0
  26. data/lib/rack/content_length.rb +3 -1
  27. data/lib/rack/content_type.rb +3 -1
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +28 -17
  30. data/lib/rack/directory.rb +17 -14
  31. data/lib/rack/etag.rb +3 -1
  32. data/lib/rack/events.rb +5 -3
  33. data/lib/rack/file.rb +5 -173
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler.rb +7 -2
  36. data/lib/rack/handler/cgi.rb +3 -1
  37. data/lib/rack/handler/fastcgi.rb +4 -2
  38. data/lib/rack/handler/lsws.rb +3 -1
  39. data/lib/rack/handler/scgi.rb +9 -6
  40. data/lib/rack/handler/thin.rb +3 -1
  41. data/lib/rack/handler/webrick.rb +4 -2
  42. data/lib/rack/head.rb +2 -0
  43. data/lib/rack/lint.rb +14 -11
  44. data/lib/rack/lobster.rb +7 -5
  45. data/lib/rack/lock.rb +2 -0
  46. data/lib/rack/logger.rb +2 -0
  47. data/lib/rack/media_type.rb +10 -5
  48. data/lib/rack/method_override.rb +9 -3
  49. data/lib/rack/mime.rb +9 -1
  50. data/lib/rack/mock.rb +74 -15
  51. data/lib/rack/multipart.rb +5 -3
  52. data/lib/rack/multipart/generator.rb +6 -7
  53. data/lib/rack/multipart/parser.rb +54 -51
  54. data/lib/rack/multipart/uploaded_file.rb +2 -0
  55. data/lib/rack/null_logger.rb +2 -0
  56. data/lib/rack/query_parser.rb +51 -25
  57. data/lib/rack/recursive.rb +7 -5
  58. data/lib/rack/reloader.rb +10 -4
  59. data/lib/rack/request.rb +89 -23
  60. data/lib/rack/response.rb +71 -31
  61. data/lib/rack/rewindable_input.rb +4 -2
  62. data/lib/rack/runtime.rb +4 -2
  63. data/lib/rack/sendfile.rb +15 -8
  64. data/lib/rack/server.rb +88 -16
  65. data/lib/rack/session/abstract/id.rb +104 -21
  66. data/lib/rack/session/cookie.rb +21 -11
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +17 -8
  69. data/lib/rack/show_exceptions.rb +16 -10
  70. data/lib/rack/show_status.rb +4 -2
  71. data/lib/rack/static.rb +15 -10
  72. data/lib/rack/tempfile_reaper.rb +2 -0
  73. data/lib/rack/urlmap.rb +11 -2
  74. data/lib/rack/utils.rb +55 -70
  75. data/rack.gemspec +19 -9
  76. metadata +32 -173
  77. data/test/builder/an_underscore_app.rb +0 -5
  78. data/test/builder/anything.rb +0 -5
  79. data/test/builder/comment.ru +0 -4
  80. data/test/builder/end.ru +0 -5
  81. data/test/builder/line.ru +0 -1
  82. data/test/builder/options.ru +0 -2
  83. data/test/cgi/assets/folder/test.js +0 -1
  84. data/test/cgi/assets/fonts/font.eot +0 -1
  85. data/test/cgi/assets/images/image.png +0 -1
  86. data/test/cgi/assets/index.html +0 -1
  87. data/test/cgi/assets/javascripts/app.js +0 -1
  88. data/test/cgi/assets/stylesheets/app.css +0 -1
  89. data/test/cgi/lighttpd.conf +0 -26
  90. data/test/cgi/rackup_stub.rb +0 -6
  91. data/test/cgi/sample_rackup.ru +0 -5
  92. data/test/cgi/test +0 -9
  93. data/test/cgi/test+directory/test+file +0 -1
  94. data/test/cgi/test.fcgi +0 -9
  95. data/test/cgi/test.gz +0 -0
  96. data/test/cgi/test.ru +0 -5
  97. data/test/gemloader.rb +0 -10
  98. data/test/helper.rb +0 -34
  99. data/test/multipart/bad_robots +0 -259
  100. data/test/multipart/binary +0 -0
  101. data/test/multipart/content_type_and_no_filename +0 -6
  102. data/test/multipart/empty +0 -10
  103. data/test/multipart/fail_16384_nofile +0 -814
  104. data/test/multipart/file1.txt +0 -1
  105. data/test/multipart/filename_and_modification_param +0 -7
  106. data/test/multipart/filename_and_no_name +0 -6
  107. data/test/multipart/filename_with_encoded_words +0 -7
  108. data/test/multipart/filename_with_escaped_quotes +0 -6
  109. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  110. data/test/multipart/filename_with_null_byte +0 -7
  111. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  112. data/test/multipart/filename_with_single_quote +0 -7
  113. data/test/multipart/filename_with_unescaped_percentages +0 -6
  114. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  115. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  116. data/test/multipart/filename_with_unescaped_quotes +0 -6
  117. data/test/multipart/ie +0 -6
  118. data/test/multipart/invalid_character +0 -6
  119. data/test/multipart/mixed_files +0 -21
  120. data/test/multipart/nested +0 -10
  121. data/test/multipart/none +0 -9
  122. data/test/multipart/quoted +0 -15
  123. data/test/multipart/rack-logo.png +0 -0
  124. data/test/multipart/semicolon +0 -6
  125. data/test/multipart/text +0 -15
  126. data/test/multipart/three_files_three_fields +0 -31
  127. data/test/multipart/unity3d_wwwform +0 -11
  128. data/test/multipart/webkit +0 -32
  129. data/test/rackup/config.ru +0 -31
  130. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  131. data/test/spec_auth_basic.rb +0 -89
  132. data/test/spec_auth_digest.rb +0 -260
  133. data/test/spec_body_proxy.rb +0 -85
  134. data/test/spec_builder.rb +0 -233
  135. data/test/spec_cascade.rb +0 -63
  136. data/test/spec_cgi.rb +0 -84
  137. data/test/spec_chunked.rb +0 -103
  138. data/test/spec_common_logger.rb +0 -95
  139. data/test/spec_conditional_get.rb +0 -103
  140. data/test/spec_config.rb +0 -23
  141. data/test/spec_content_length.rb +0 -86
  142. data/test/spec_content_type.rb +0 -46
  143. data/test/spec_deflater.rb +0 -375
  144. data/test/spec_directory.rb +0 -148
  145. data/test/spec_etag.rb +0 -108
  146. data/test/spec_events.rb +0 -133
  147. data/test/spec_fastcgi.rb +0 -85
  148. data/test/spec_file.rb +0 -264
  149. data/test/spec_handler.rb +0 -57
  150. data/test/spec_head.rb +0 -46
  151. data/test/spec_lint.rb +0 -515
  152. data/test/spec_lobster.rb +0 -59
  153. data/test/spec_lock.rb +0 -204
  154. data/test/spec_logger.rb +0 -24
  155. data/test/spec_media_type.rb +0 -42
  156. data/test/spec_method_override.rb +0 -96
  157. data/test/spec_mime.rb +0 -51
  158. data/test/spec_mock.rb +0 -359
  159. data/test/spec_multipart.rb +0 -722
  160. data/test/spec_null_logger.rb +0 -21
  161. data/test/spec_recursive.rb +0 -75
  162. data/test/spec_request.rb +0 -1393
  163. data/test/spec_response.rb +0 -510
  164. data/test/spec_rewindable_input.rb +0 -128
  165. data/test/spec_runtime.rb +0 -50
  166. data/test/spec_sendfile.rb +0 -125
  167. data/test/spec_server.rb +0 -193
  168. data/test/spec_session_abstract_id.rb +0 -31
  169. data/test/spec_session_abstract_session_hash.rb +0 -45
  170. data/test/spec_session_cookie.rb +0 -442
  171. data/test/spec_session_memcache.rb +0 -320
  172. data/test/spec_session_pool.rb +0 -210
  173. data/test/spec_show_exceptions.rb +0 -80
  174. data/test/spec_show_status.rb +0 -104
  175. data/test/spec_static.rb +0 -184
  176. data/test/spec_tempfile_reaper.rb +0 -64
  177. data/test/spec_thin.rb +0 -96
  178. data/test/spec_urlmap.rb +0 -237
  179. data/test/spec_utils.rb +0 -742
  180. data/test/spec_version.rb +0 -11
  181. data/test/spec_webrick.rb +0 -206
  182. data/test/static/another/index.html +0 -1
  183. data/test/static/foo.html +0 -1
  184. data/test/static/index.html +0 -1
  185. data/test/testrequest.rb +0 -78
  186. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  187. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Rack
@@ -10,14 +12,14 @@ module Rack
10
12
  class ForwardRequest < Exception
11
13
  attr_reader :url, :env
12
14
 
13
- def initialize(url, env={})
15
+ def initialize(url, env = {})
14
16
  @url = URI(url)
15
17
  @env = env
16
18
 
17
- @env[PATH_INFO] = @url.path
18
- @env[QUERY_STRING] = @url.query if @url.query
19
- @env[HTTP_HOST] = @url.host if @url.host
20
- @env["HTTP_PORT"] = @url.port if @url.port
19
+ @env[PATH_INFO] = @url.path
20
+ @env[QUERY_STRING] = @url.query if @url.query
21
+ @env[HTTP_HOST] = @url.host if @url.host
22
+ @env["HTTP_PORT"] = @url.port if @url.port
21
23
  @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
22
24
 
23
25
  super "forwarding to #{url}"
@@ -1,9 +1,13 @@
1
- # Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
2
- # Rack::Reloader is subject to the terms of an MIT-style license.
3
- # See COPYING or http://www.opensource.org/licenses/mit-license.php.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2009-2018 Michael Fellinger <m.fellinger@gmail.com>
4
+ # Rack::Reloader is subject to the terms of an MIT-style license.
5
+ # See MIT-LICENSE or https://opensource.org/licenses/MIT.
4
6
 
5
7
  require 'pathname'
6
8
 
9
+ require_relative 'core_ext/regexp'
10
+
7
11
  module Rack
8
12
 
9
13
  # High performant source reloader
@@ -20,6 +24,8 @@ module Rack
20
24
  # It is performing a check/reload cycle at the start of every request, but
21
25
  # also respects a cool down time, during which nothing will be done.
22
26
  class Reloader
27
+ using ::Rack::RegexpExtensions
28
+
23
29
  def initialize(app, cooldown = 10, backend = Stat)
24
30
  @app = app
25
31
  @cooldown = cooldown
@@ -69,7 +75,7 @@ module Rack
69
75
  paths = ['./', *$LOAD_PATH].uniq
70
76
 
71
77
  files.map{|file|
72
- next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
78
+ next if /\.(so|bundle)$/.match?(file) # cannot reload compiled files
73
79
 
74
80
  found, stat = figure_path(file, paths)
75
81
  next unless found && stat && mtime = stat.mtime
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/utils'
2
4
  require 'rack/media_type'
3
5
 
6
+ require_relative 'core_ext/regexp'
7
+
4
8
  module Rack
5
9
  # Rack::Request provides a convenient interface to a Rack
6
10
  # environment. It is stateless, the environment +env+ passed to the
@@ -11,6 +15,19 @@ module Rack
11
15
  # req.params["data"]
12
16
 
13
17
  class Request
18
+ using ::Rack::RegexpExtensions
19
+
20
+ class << self
21
+ attr_accessor :ip_filter
22
+ end
23
+
24
+ self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) }
25
+ ALLOWED_SCHEMES = %w(https http).freeze
26
+ SCHEME_WHITELIST = ALLOWED_SCHEMES
27
+ if Object.respond_to?(:deprecate_constant)
28
+ deprecate_constant :SCHEME_WHITELIST
29
+ end
30
+
14
31
  def initialize(env)
15
32
  @params = nil
16
33
  super(env)
@@ -98,7 +115,7 @@ module Rack
98
115
 
99
116
  module Helpers
100
117
  # The set of form-data media-types. Requests that do not indicate
101
- # one of the media types presents in this list will not be eligible
118
+ # one of the media types present in this list will not be eligible
102
119
  # for form-data / param parsing.
103
120
  FORM_DATA_MEDIA_TYPES = [
104
121
  'application/x-www-form-urlencoded',
@@ -106,7 +123,7 @@ module Rack
106
123
  ]
107
124
 
108
125
  # The set of media-types. Requests that do not indicate
109
- # one of the media types presents in this list will not be eligible
126
+ # one of the media types present in this list will not be eligible
110
127
  # for param parsing like soap attachments or generic multiparts
111
128
  PARSEABLE_DATA_MEDIA_TYPES = [
112
129
  'multipart/related',
@@ -117,11 +134,11 @@ module Rack
117
134
  # to include the port in a generated URI.
118
135
  DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
119
136
 
120
- HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze
121
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze
122
- HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'.freeze
123
- HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'.freeze
124
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'.freeze
137
+ 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'
125
142
 
126
143
  def body; get_header(RACK_INPUT) end
127
144
  def script_name; get_header(SCRIPT_NAME).to_s end
@@ -157,10 +174,10 @@ module Rack
157
174
  def delete?; request_method == DELETE end
158
175
 
159
176
  # Checks the HTTP request method (or verb) to see if it was of type GET
160
- def get?; request_method == GET end
177
+ def get?; request_method == GET end
161
178
 
162
179
  # Checks the HTTP request method (or verb) to see if it was of type HEAD
163
- def head?; request_method == HEAD end
180
+ def head?; request_method == HEAD end
164
181
 
165
182
  # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
166
183
  def options?; request_method == OPTIONS end
@@ -188,10 +205,8 @@ module Rack
188
205
  'https'
189
206
  elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
190
207
  'https'
191
- elsif get_header(HTTP_X_FORWARDED_SCHEME)
192
- get_header(HTTP_X_FORWARDED_SCHEME)
193
- elsif get_header(HTTP_X_FORWARDED_PROTO)
194
- get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]
208
+ elsif forwarded_scheme
209
+ forwarded_scheme
195
210
  else
196
211
  get_header(RACK_URL_SCHEME)
197
212
  end
@@ -208,7 +223,7 @@ module Rack
208
223
  string = get_header HTTP_COOKIE
209
224
 
210
225
  return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
211
- hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
226
+ hash.replace Utils.parse_cookies_header string
212
227
  set_header(RACK_REQUEST_COOKIE_STRING, string)
213
228
  hash
214
229
  end
@@ -232,18 +247,23 @@ module Rack
232
247
 
233
248
  def host
234
249
  # Remove port number.
235
- host_with_port.to_s.sub(/:\d+\z/, '')
250
+ h = host_with_port
251
+ if colon_index = h.index(":")
252
+ h[0, colon_index]
253
+ else
254
+ h
255
+ end
236
256
  end
237
257
 
238
258
  def port
239
- if port = host_with_port.split(/:/)[1]
259
+ if port = extract_port(host_with_port)
240
260
  port.to_i
241
261
  elsif port = get_header(HTTP_X_FORWARDED_PORT)
242
262
  port.to_i
243
263
  elsif has_header?(HTTP_X_FORWARDED_HOST)
244
264
  DEFAULT_PORTS[scheme]
245
265
  elsif has_header?(HTTP_X_FORWARDED_PROTO)
246
- DEFAULT_PORTS[get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]]
266
+ DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))]
247
267
  else
248
268
  get_header(SERVER_PORT).to_i
249
269
  end
@@ -260,8 +280,9 @@ module Rack
260
280
  return remote_addrs.first if remote_addrs.any?
261
281
 
262
282
  forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
283
+ .map { |ip| strip_port(ip) }
263
284
 
264
- return reject_trusted_ip_addresses(forwarded_ips).last || get_header("REMOTE_ADDR")
285
+ return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
265
286
  end
266
287
 
267
288
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -337,7 +358,7 @@ module Rack
337
358
 
338
359
  # Fix for Safari Ajax postings that always append \0
339
360
  # form_vars.sub!(/\0\z/, '') # performance replacement:
340
- form_vars.slice!(-1) if form_vars[-1] == ?\0
361
+ form_vars.slice!(-1) if form_vars.end_with?("\0")
341
362
 
342
363
  set_header RACK_REQUEST_FORM_VARS, form_vars
343
364
  set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
@@ -386,12 +407,13 @@ module Rack
386
407
  #
387
408
  # <tt>env['rack.input']</tt> is not touched.
388
409
  def delete_param(k)
389
- [ self.POST.delete(k), self.GET.delete(k) ].compact.first
410
+ post_value, get_value = self.POST.delete(k), self.GET.delete(k)
411
+ post_value || get_value
390
412
  end
391
413
 
392
414
  def base_url
393
415
  url = "#{scheme}://#{host}"
394
- url << ":#{port}" if port != DEFAULT_PORTS[scheme]
416
+ url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
395
417
  url
396
418
  end
397
419
 
@@ -417,7 +439,7 @@ module Rack
417
439
  end
418
440
 
419
441
  def trusted_proxy?(ip)
420
- ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
442
+ Rack::Request.ip_filter.call(ip)
421
443
  end
422
444
 
423
445
  # shortcut for <tt>request.params[key]</tt>
@@ -464,7 +486,7 @@ module Rack
464
486
  Utils.default_query_parser
465
487
  end
466
488
 
467
- def parse_query(qs, d='&')
489
+ def parse_query(qs, d = '&')
468
490
  query_parser.parse_nested_query(qs, d)
469
491
  end
470
492
 
@@ -476,9 +498,53 @@ module Rack
476
498
  ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
477
499
  end
478
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]
515
+ end
516
+
517
+ ip_address
518
+ end
519
+
479
520
  def reject_trusted_ip_addresses(ip_addresses)
480
521
  ip_addresses.reject { |ip| trusted_proxy?(ip) }
481
522
  end
523
+
524
+ def forwarded_scheme
525
+ allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
526
+ allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
527
+ end
528
+
529
+ def allowed_scheme(header)
530
+ header if ALLOWED_SCHEMES.include?(header)
531
+ end
532
+
533
+ def extract_proto_header(header)
534
+ if header
535
+ if (comma_index = header.index(','))
536
+ header[0, comma_index]
537
+ else
538
+ header
539
+ end
540
+ end
541
+ end
542
+
543
+ def extract_port(uri)
544
+ if (colon_index = uri.index(':'))
545
+ uri[colon_index + 1, uri.length]
546
+ end
547
+ end
482
548
  end
483
549
 
484
550
  include Env
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/request'
2
4
  require 'rack/utils'
3
5
  require 'rack/body_proxy'
@@ -23,32 +25,34 @@ module Rack
23
25
  attr_reader :header
24
26
  alias headers header
25
27
 
26
- CHUNKED = 'chunked'.freeze
28
+ CHUNKED = 'chunked'
29
+ STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
27
30
 
28
- def initialize(body=[], status=200, header={})
31
+ def initialize(body = nil, status = 200, header = {})
29
32
  @status = status.to_i
30
- @header = Utils::HeaderHash.new.merge(header)
33
+ @header = Utils::HeaderHash.new(header)
31
34
 
32
- @writer = lambda { |x| @body << x }
33
- @block = nil
34
- @length = 0
35
+ @writer = self.method(:append)
35
36
 
36
- @body = []
37
+ @block = nil
38
+ @length = 0
37
39
 
38
- if body.respond_to? :to_str
39
- write body.to_str
40
- elsif body.respond_to?(:each)
41
- body.each { |part|
42
- write part.to_s
43
- }
40
+ # Keep track of whether we have expanded the user supplied body.
41
+ if body.nil?
42
+ @body = []
43
+ @buffered = true
44
+ elsif body.respond_to?(:to_str)
45
+ @body = [body]
46
+ @buffered = true
44
47
  else
45
- raise TypeError, "stringable or iterable required"
48
+ @body = body
49
+ @buffered = false
46
50
  end
47
51
 
48
- yield self if block_given?
52
+ yield self if block_given?
49
53
  end
50
54
 
51
- def redirect(target, status=302)
55
+ def redirect(target, status = 302)
52
56
  self.status = status
53
57
  self.location = target
54
58
  end
@@ -58,41 +62,45 @@ module Rack
58
62
  end
59
63
 
60
64
  def finish(&block)
61
- @block = block
62
-
63
- if [204, 304].include?(status.to_i)
65
+ if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
64
66
  delete_header CONTENT_TYPE
65
67
  delete_header CONTENT_LENGTH
66
68
  close
67
69
  [status.to_i, header, []]
68
70
  else
69
- [status.to_i, header, BodyProxy.new(self){}]
71
+ if block_given?
72
+ @block = block
73
+ [status.to_i, header, self]
74
+ else
75
+ [status.to_i, header, @body]
76
+ end
70
77
  end
71
78
  end
79
+
72
80
  alias to_a finish # For *response
73
- alias to_ary finish # For implicit-splat on Ruby 1.9.2
74
81
 
75
82
  def each(&callback)
76
83
  @body.each(&callback)
77
- @writer = callback
78
- @block.call(self) if @block
84
+ @buffered = true
85
+
86
+ if @block
87
+ @writer = callback
88
+ @block.call(self)
89
+ end
79
90
  end
80
91
 
81
92
  # Append to body and update Content-Length.
82
93
  #
83
94
  # NOTE: Do not mix #write and direct #body access!
84
95
  #
85
- def write(str)
86
- s = str.to_s
87
- @length += s.bytesize unless chunked?
88
- @writer.call s
96
+ def write(chunk)
97
+ buffered_body!
89
98
 
90
- set_header(CONTENT_LENGTH, @length.to_s) unless chunked?
91
- str
99
+ @writer.call(chunk.to_s)
92
100
  end
93
101
 
94
102
  def close
95
- body.close if body.respond_to?(:close)
103
+ @body.close if @body.respond_to?(:close)
96
104
  end
97
105
 
98
106
  def empty?
@@ -184,7 +192,7 @@ module Rack
184
192
  set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
185
193
  end
186
194
 
187
- def delete_cookie(key, value={})
195
+ def delete_cookie(key, value = {})
188
196
  set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
189
197
  end
190
198
 
@@ -211,6 +219,38 @@ module Rack
211
219
  def etag= v
212
220
  set_header ETAG, v
213
221
  end
222
+
223
+ protected
224
+
225
+ def buffered_body!
226
+ return if @buffered
227
+
228
+ if @body.is_a?(Array)
229
+ # The user supplied body was an array:
230
+ @body = @body.compact
231
+ else
232
+ # Turn the user supplied body into a buffered array:
233
+ body = @body
234
+ @body = Array.new
235
+
236
+ body.each do |part|
237
+ @writer.call(part.to_s)
238
+ end
239
+ end
240
+
241
+ @buffered = true
242
+ end
243
+
244
+ def append(chunk)
245
+ @body << chunk
246
+
247
+ unless chunked?
248
+ @length += chunk.bytesize
249
+ set_header(CONTENT_LENGTH, @length.to_s)
250
+ end
251
+
252
+ return chunk
253
+ end
214
254
  end
215
255
 
216
256
  include Helpers