rack 1.1.6 → 1.6.9

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.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +1 -1
  3. data/HISTORY.md +375 -0
  4. data/KNOWN-ISSUES +23 -0
  5. data/README.rdoc +312 -0
  6. data/Rakefile +124 -0
  7. data/SPEC +125 -32
  8. data/contrib/rack.png +0 -0
  9. data/contrib/rack.svg +150 -0
  10. data/contrib/rack_logo.svg +1 -1
  11. data/contrib/rdoc.css +412 -0
  12. data/example/protectedlobster.rb +1 -1
  13. data/lib/rack/auth/abstract/handler.rb +4 -4
  14. data/lib/rack/auth/abstract/request.rb +7 -5
  15. data/lib/rack/auth/basic.rb +1 -1
  16. data/lib/rack/auth/digest/md5.rb +7 -3
  17. data/lib/rack/auth/digest/nonce.rb +1 -1
  18. data/lib/rack/auth/digest/params.rb +7 -9
  19. data/lib/rack/auth/digest/request.rb +10 -9
  20. data/lib/rack/backports/uri/common_18.rb +56 -0
  21. data/lib/rack/backports/uri/common_192.rb +52 -0
  22. data/lib/rack/backports/uri/common_193.rb +29 -0
  23. data/lib/rack/body_proxy.rb +39 -0
  24. data/lib/rack/builder.rb +106 -22
  25. data/lib/rack/cascade.rb +17 -6
  26. data/lib/rack/chunked.rb +44 -24
  27. data/lib/rack/commonlogger.rb +36 -13
  28. data/lib/rack/conditionalget.rb +49 -17
  29. data/lib/rack/config.rb +5 -0
  30. data/lib/rack/content_length.rb +14 -6
  31. data/lib/rack/content_type.rb +7 -1
  32. data/lib/rack/deflater.rb +73 -15
  33. data/lib/rack/directory.rb +18 -8
  34. data/lib/rack/etag.rb +59 -9
  35. data/lib/rack/file.rb +106 -44
  36. data/lib/rack/handler/cgi.rb +11 -11
  37. data/lib/rack/handler/fastcgi.rb +18 -6
  38. data/lib/rack/handler/lsws.rb +2 -4
  39. data/lib/rack/handler/mongrel.rb +22 -6
  40. data/lib/rack/handler/scgi.rb +16 -8
  41. data/lib/rack/handler/thin.rb +19 -4
  42. data/lib/rack/handler/webrick.rb +72 -19
  43. data/lib/rack/handler.rb +47 -14
  44. data/lib/rack/head.rb +10 -2
  45. data/lib/rack/lint.rb +260 -75
  46. data/lib/rack/lobster.rb +13 -8
  47. data/lib/rack/lock.rb +13 -3
  48. data/lib/rack/logger.rb +0 -2
  49. data/lib/rack/methodoverride.rb +27 -8
  50. data/lib/rack/mime.rb +625 -167
  51. data/lib/rack/mock.rb +78 -53
  52. data/lib/rack/multipart/generator.rb +93 -0
  53. data/lib/rack/multipart/parser.rb +253 -0
  54. data/lib/rack/multipart/uploaded_file.rb +34 -0
  55. data/lib/rack/multipart.rb +34 -0
  56. data/lib/rack/nulllogger.rb +21 -2
  57. data/lib/rack/recursive.rb +10 -5
  58. data/lib/rack/reloader.rb +3 -2
  59. data/lib/rack/request.rb +201 -74
  60. data/lib/rack/response.rb +41 -28
  61. data/lib/rack/rewindable_input.rb +15 -11
  62. data/lib/rack/runtime.rb +16 -3
  63. data/lib/rack/sendfile.rb +47 -29
  64. data/lib/rack/server.rb +223 -47
  65. data/lib/rack/session/abstract/id.rb +289 -30
  66. data/lib/rack/session/cookie.rb +133 -44
  67. data/lib/rack/session/memcache.rb +30 -56
  68. data/lib/rack/session/pool.rb +19 -43
  69. data/lib/rack/showexceptions.rb +53 -15
  70. data/lib/rack/showstatus.rb +14 -7
  71. data/lib/rack/static.rb +124 -12
  72. data/lib/rack/tempfile_reaper.rb +22 -0
  73. data/lib/rack/urlmap.rb +49 -15
  74. data/lib/rack/utils/okjson.rb +600 -0
  75. data/lib/rack/utils.rb +363 -361
  76. data/lib/rack.rb +17 -23
  77. data/rack.gemspec +11 -20
  78. data/test/builder/anything.rb +5 -0
  79. data/test/builder/comment.ru +4 -0
  80. data/test/builder/end.ru +5 -0
  81. data/test/builder/line.ru +1 -0
  82. data/test/builder/options.ru +2 -0
  83. data/test/cgi/assets/folder/test.js +1 -0
  84. data/test/cgi/assets/fonts/font.eot +1 -0
  85. data/test/cgi/assets/images/image.png +1 -0
  86. data/test/cgi/assets/index.html +1 -0
  87. data/test/cgi/assets/javascripts/app.js +1 -0
  88. data/test/cgi/assets/stylesheets/app.css +1 -0
  89. data/test/cgi/lighttpd.conf +26 -0
  90. data/test/cgi/rackup_stub.rb +6 -0
  91. data/test/cgi/sample_rackup.ru +5 -0
  92. data/test/cgi/test +9 -0
  93. data/test/cgi/test+directory/test+file +1 -0
  94. data/test/cgi/test.fcgi +8 -0
  95. data/test/cgi/test.ru +5 -0
  96. data/test/gemloader.rb +10 -0
  97. data/test/multipart/bad_robots +259 -0
  98. data/test/multipart/binary +0 -0
  99. data/test/multipart/content_type_and_no_filename +6 -0
  100. data/test/multipart/empty +10 -0
  101. data/test/multipart/fail_16384_nofile +814 -0
  102. data/test/multipart/file1.txt +1 -0
  103. data/test/multipart/filename_and_modification_param +7 -0
  104. data/test/multipart/filename_and_no_name +6 -0
  105. data/test/multipart/filename_with_escaped_quotes +6 -0
  106. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  107. data/test/multipart/filename_with_null_byte +7 -0
  108. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  109. data/test/multipart/filename_with_unescaped_percentages +6 -0
  110. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  111. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  112. data/test/multipart/filename_with_unescaped_quotes +6 -0
  113. data/test/multipart/ie +6 -0
  114. data/test/multipart/invalid_character +6 -0
  115. data/test/multipart/mixed_files +21 -0
  116. data/test/multipart/nested +10 -0
  117. data/test/multipart/none +9 -0
  118. data/test/multipart/semicolon +6 -0
  119. data/test/multipart/text +15 -0
  120. data/test/multipart/three_files_three_fields +31 -0
  121. data/test/multipart/webkit +32 -0
  122. data/test/rackup/config.ru +31 -0
  123. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  124. data/test/{spec_rack_auth_basic.rb → spec_auth_basic.rb} +23 -15
  125. data/test/{spec_rack_auth_digest.rb → spec_auth_digest.rb} +56 -29
  126. data/test/spec_body_proxy.rb +85 -0
  127. data/test/spec_builder.rb +223 -0
  128. data/test/{spec_rack_cascade.rb → spec_cascade.rb} +28 -15
  129. data/test/{spec_rack_cgi.rb → spec_cgi.rb} +44 -31
  130. data/test/spec_chunked.rb +101 -0
  131. data/test/spec_commonlogger.rb +93 -0
  132. data/test/spec_conditionalget.rb +102 -0
  133. data/test/{spec_rack_config.rb → spec_config.rb} +6 -8
  134. data/test/spec_content_length.rb +85 -0
  135. data/test/spec_content_type.rb +45 -0
  136. data/test/spec_deflater.rb +339 -0
  137. data/test/{spec_rack_directory.rb → spec_directory.rb} +37 -10
  138. data/test/spec_etag.rb +107 -0
  139. data/test/{spec_rack_fastcgi.rb → spec_fastcgi.rb} +47 -29
  140. data/test/spec_file.rb +221 -0
  141. data/test/spec_handler.rb +72 -0
  142. data/test/spec_head.rb +45 -0
  143. data/test/{spec_rack_lint.rb → spec_lint.rb} +82 -60
  144. data/test/spec_lobster.rb +58 -0
  145. data/test/spec_lock.rb +164 -0
  146. data/test/spec_logger.rb +23 -0
  147. data/test/spec_methodoverride.rb +95 -0
  148. data/test/spec_mime.rb +51 -0
  149. data/test/{spec_rack_mock.rb → spec_mock.rb} +92 -38
  150. data/test/{spec_rack_mongrel.rb → spec_mongrel.rb} +46 -53
  151. data/test/spec_multipart.rb +600 -0
  152. data/test/spec_nulllogger.rb +20 -0
  153. data/test/spec_recursive.rb +72 -0
  154. data/test/spec_request.rb +1227 -0
  155. data/test/spec_response.rb +407 -0
  156. data/test/spec_rewindable_input.rb +118 -0
  157. data/test/spec_runtime.rb +49 -0
  158. data/test/spec_sendfile.rb +130 -0
  159. data/test/spec_server.rb +167 -0
  160. data/test/spec_session_abstract_id.rb +53 -0
  161. data/test/spec_session_cookie.rb +410 -0
  162. data/test/{spec_rack_session_memcache.rb → spec_session_memcache.rb} +119 -71
  163. data/test/{spec_rack_session_pool.rb → spec_session_pool.rb} +106 -69
  164. data/test/spec_showexceptions.rb +85 -0
  165. data/test/spec_showstatus.rb +103 -0
  166. data/test/spec_static.rb +145 -0
  167. data/test/spec_tempfile_reaper.rb +63 -0
  168. data/test/{spec_rack_thin.rb → spec_thin.rb} +35 -35
  169. data/test/{spec_rack_urlmap.rb → spec_urlmap.rb} +40 -19
  170. data/test/spec_utils.rb +647 -0
  171. data/test/spec_version.rb +17 -0
  172. data/test/spec_webrick.rb +184 -0
  173. data/test/static/another/index.html +1 -0
  174. data/test/static/index.html +1 -0
  175. data/test/testrequest.rb +78 -0
  176. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  177. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  178. metadata +220 -239
  179. data/RDOX +0 -0
  180. data/README +0 -592
  181. data/lib/rack/adapter/camping.rb +0 -22
  182. data/test/spec_auth.rb +0 -57
  183. data/test/spec_rack_builder.rb +0 -84
  184. data/test/spec_rack_camping.rb +0 -55
  185. data/test/spec_rack_chunked.rb +0 -62
  186. data/test/spec_rack_commonlogger.rb +0 -61
  187. data/test/spec_rack_conditionalget.rb +0 -41
  188. data/test/spec_rack_content_length.rb +0 -43
  189. data/test/spec_rack_content_type.rb +0 -30
  190. data/test/spec_rack_deflater.rb +0 -127
  191. data/test/spec_rack_etag.rb +0 -17
  192. data/test/spec_rack_file.rb +0 -75
  193. data/test/spec_rack_handler.rb +0 -43
  194. data/test/spec_rack_head.rb +0 -30
  195. data/test/spec_rack_lobster.rb +0 -45
  196. data/test/spec_rack_lock.rb +0 -38
  197. data/test/spec_rack_logger.rb +0 -21
  198. data/test/spec_rack_methodoverride.rb +0 -60
  199. data/test/spec_rack_nulllogger.rb +0 -13
  200. data/test/spec_rack_recursive.rb +0 -77
  201. data/test/spec_rack_request.rb +0 -594
  202. data/test/spec_rack_response.rb +0 -221
  203. data/test/spec_rack_rewindable_input.rb +0 -118
  204. data/test/spec_rack_runtime.rb +0 -35
  205. data/test/spec_rack_sendfile.rb +0 -86
  206. data/test/spec_rack_session_cookie.rb +0 -92
  207. data/test/spec_rack_showexceptions.rb +0 -21
  208. data/test/spec_rack_showstatus.rb +0 -72
  209. data/test/spec_rack_static.rb +0 -37
  210. data/test/spec_rack_utils.rb +0 -557
  211. data/test/spec_rack_webrick.rb +0 -130
  212. data/test/spec_rackup.rb +0 -164
data/lib/rack/utils.rb CHANGED
@@ -1,28 +1,59 @@
1
1
  # -*- encoding: binary -*-
2
-
2
+ require 'fileutils'
3
3
  require 'set'
4
4
  require 'tempfile'
5
+ require 'rack/multipart'
6
+ require 'time'
7
+
8
+ major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
9
+
10
+ if major == 1 && minor < 9
11
+ require 'rack/backports/uri/common_18'
12
+ elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby'
13
+ require 'rack/backports/uri/common_192'
14
+ elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
15
+ require 'rack/backports/uri/common_193'
16
+ else
17
+ require 'uri/common'
18
+ end
5
19
 
6
20
  module Rack
7
21
  # Rack::Utils contains a grab-bag of useful methods for writing web
8
22
  # applications adopted from all kinds of Ruby libraries.
9
23
 
10
24
  module Utils
11
- # Performs URI escaping so that you can construct proper
12
- # query strings faster. Use this rather than the cgi.rb
13
- # version since it's faster. (Stolen from Camping).
25
+ # ParameterTypeError is the error that is raised when incoming structural
26
+ # parameters (parsed by parse_nested_query) contain conflicting types.
27
+ class ParameterTypeError < TypeError; end
28
+
29
+ # InvalidParameterError is the error that is raised when incoming structural
30
+ # parameters (parsed by parse_nested_query) contain invalid format or byte
31
+ # sequence.
32
+ class InvalidParameterError < ArgumentError; end
33
+
34
+ # URI escapes. (CGI style space to +)
14
35
  def escape(s)
15
- s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
16
- '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
17
- }.tr(' ', '+')
36
+ URI.encode_www_form_component(s)
18
37
  end
19
38
  module_function :escape
20
39
 
21
- # Unescapes a URI escaped string. (Stolen from Camping).
22
- def unescape(s)
23
- s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
24
- [$1.delete('%')].pack('H*')
25
- }
40
+ # Like URI escaping, but with %20 instead of +. Strictly speaking this is
41
+ # true URI escaping.
42
+ def escape_path(s)
43
+ escape(s).gsub('+', '%20')
44
+ end
45
+ module_function :escape_path
46
+
47
+ # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
48
+ # target encoding of the string returned, and it defaults to UTF-8
49
+ if defined?(::Encoding)
50
+ def unescape(s, encoding = Encoding::UTF_8)
51
+ URI.decode_www_form_component(s, encoding)
52
+ end
53
+ else
54
+ def unescape(s, encoding = nil)
55
+ URI.decode_www_form_component(s, encoding)
56
+ end
26
57
  end
27
58
  module_function :unescape
28
59
 
@@ -30,32 +61,37 @@ module Rack
30
61
 
31
62
  class << self
32
63
  attr_accessor :key_space_limit
64
+ attr_accessor :param_depth_limit
65
+ attr_accessor :multipart_part_limit
33
66
  end
34
67
 
35
68
  # The default number of bytes to allow parameter keys to take up.
36
69
  # This helps prevent a rogue client from flooding a Request.
37
70
  self.key_space_limit = 65536
38
71
 
72
+ # Default depth at which the parameter parser will raise an exception for
73
+ # being too deep. This helps prevent SystemStackErrors
74
+ self.param_depth_limit = 100
75
+
76
+ # The maximum number of parts a request can contain. Accepting too many part
77
+ # can lead to the server running out of file handles.
78
+ # Set to `0` for no limit.
79
+ # FIXME: RACK_MULTIPART_LIMIT was introduced by mistake and it will be removed in 1.7.0
80
+ self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_LIMIT'] || 128).to_i
81
+
39
82
  # Stolen from Mongrel, with some small modifications:
40
83
  # Parses a query string by breaking it up at the '&'
41
84
  # and ';' characters. You can also use this to parse
42
85
  # cookies by changing the characters used in the second
43
86
  # parameter (which defaults to '&;').
44
- def parse_query(qs, d = nil)
45
- params = {}
87
+ def parse_query(qs, d = nil, &unescaper)
88
+ unescaper ||= method(:unescape)
46
89
 
47
- max_key_space = Utils.key_space_limit
48
- bytes = 0
90
+ params = KeySpaceConstrainedParams.new
49
91
 
50
92
  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
51
- k, v = p.split('=', 2).map { |x| unescape(x) }
52
-
53
- if k
54
- bytes += k.size
55
- if bytes > max_key_space
56
- raise RangeError, "exceeded available parameter key space"
57
- end
58
- end
93
+ next if p.empty?
94
+ k, v = p.split('=', 2).map(&unescaper)
59
95
 
60
96
  if cur = params[k]
61
97
  if cur.class == Array
@@ -68,34 +104,36 @@ module Rack
68
104
  end
69
105
  end
70
106
 
71
- return params
107
+ return params.to_params_hash
72
108
  end
73
109
  module_function :parse_query
74
110
 
111
+ # parse_nested_query expands a query string into structural types. Supported
112
+ # types are Arrays, Hashes and basic value types. It is possible to supply
113
+ # query strings with parameters of conflicting types, in this case a
114
+ # ParameterTypeError is raised. Users are encouraged to return a 400 in this
115
+ # case.
75
116
  def parse_nested_query(qs, d = nil)
76
- params = {}
77
-
78
- max_key_space = Utils.key_space_limit
79
- bytes = 0
117
+ params = KeySpaceConstrainedParams.new
80
118
 
81
119
  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
82
- k, v = unescape(p).split('=', 2)
83
-
84
- if k
85
- bytes += k.size
86
- if bytes > max_key_space
87
- raise RangeError, "exceeded available parameter key space"
88
- end
89
- end
120
+ k, v = p.split('=', 2).map { |s| unescape(s) }
90
121
 
91
122
  normalize_params(params, k, v)
92
123
  end
93
124
 
94
- return params
125
+ return params.to_params_hash
126
+ rescue ArgumentError => e
127
+ raise InvalidParameterError, e.message
95
128
  end
96
129
  module_function :parse_nested_query
97
130
 
98
- def normalize_params(params, name, v = nil)
131
+ # normalize_params recursively expands parameters into structural types. If
132
+ # the structural types represented by two different parameter names are in
133
+ # conflict, a ParameterTypeError is raised.
134
+ def normalize_params(params, name, v = nil, depth = Utils.param_depth_limit)
135
+ raise RangeError if depth <= 0
136
+
99
137
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
100
138
  k = $1 || ''
101
139
  after = $' || ''
@@ -104,35 +142,42 @@ module Rack
104
142
 
105
143
  if after == ""
106
144
  params[k] = v
145
+ elsif after == "["
146
+ params[name] = v
107
147
  elsif after == "[]"
108
148
  params[k] ||= []
109
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
149
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
110
150
  params[k] << v
111
151
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
112
152
  child_key = $1
113
153
  params[k] ||= []
114
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
115
- if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
116
- normalize_params(params[k].last, child_key, v)
154
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
155
+ if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
156
+ normalize_params(params[k].last, child_key, v, depth - 1)
117
157
  else
118
- params[k] << normalize_params({}, child_key, v)
158
+ params[k] << normalize_params(params.class.new, child_key, v, depth - 1)
119
159
  end
120
160
  else
121
- params[k] ||= {}
122
- raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
123
- params[k] = normalize_params(params[k], after, v)
161
+ params[k] ||= params.class.new
162
+ raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
163
+ params[k] = normalize_params(params[k], after, v, depth - 1)
124
164
  end
125
165
 
126
166
  return params
127
167
  end
128
168
  module_function :normalize_params
129
169
 
170
+ def params_hash_type?(obj)
171
+ obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash)
172
+ end
173
+ module_function :params_hash_type?
174
+
130
175
  def build_query(params)
131
176
  params.map { |k, v|
132
177
  if v.class == Array
133
178
  build_query(v.map { |x| [k, x] })
134
179
  else
135
- "#{escape(k)}=#{escape(v)}"
180
+ v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
136
181
  end
137
182
  }.join("&")
138
183
  end
@@ -147,23 +192,61 @@ module Rack
147
192
  when Hash
148
193
  value.map { |k, v|
149
194
  build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
150
- }.join("&")
151
- when String
195
+ }.reject(&:empty?).join('&')
196
+ when nil
197
+ prefix
198
+ else
152
199
  raise ArgumentError, "value must be a Hash" if prefix.nil?
153
200
  "#{prefix}=#{escape(value)}"
154
- else
155
- prefix
156
201
  end
157
202
  end
158
203
  module_function :build_nested_query
159
204
 
205
+ def q_values(q_value_header)
206
+ q_value_header.to_s.split(/\s*,\s*/).map do |part|
207
+ value, parameters = part.split(/\s*;\s*/, 2)
208
+ quality = 1.0
209
+ if md = /\Aq=([\d.]+)/.match(parameters)
210
+ quality = md[1].to_f
211
+ end
212
+ [value, quality]
213
+ end
214
+ end
215
+ module_function :q_values
216
+
217
+ def best_q_match(q_value_header, available_mimes)
218
+ values = q_values(q_value_header)
219
+
220
+ matches = values.map do |req_mime, quality|
221
+ match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
222
+ next unless match
223
+ [match, quality]
224
+ end.compact.sort_by do |match, quality|
225
+ (match.split('/', 2).count('*') * -10) + quality
226
+ end.last
227
+ matches && matches.first
228
+ end
229
+ module_function :best_q_match
230
+
231
+ ESCAPE_HTML = {
232
+ "&" => "&amp;",
233
+ "<" => "&lt;",
234
+ ">" => "&gt;",
235
+ "'" => "&#x27;",
236
+ '"' => "&quot;",
237
+ "/" => "&#x2F;"
238
+ }
239
+ if //.respond_to?(:encoding)
240
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
241
+ else
242
+ # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwise
243
+ # TODO doesn't apply to jruby, so a better condition above might be preferable?
244
+ ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
245
+ end
246
+
160
247
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
161
248
  def escape_html(string)
162
- string.to_s.gsub("&", "&amp;").
163
- gsub("<", "&lt;").
164
- gsub(">", "&gt;").
165
- gsub("'", "&#39;").
166
- gsub('"', "&quot;")
249
+ string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
167
250
  end
168
251
  module_function :escape_html
169
252
 
@@ -187,10 +270,8 @@ module Rack
187
270
  encoding_candidates.push("identity")
188
271
  end
189
272
 
190
- expanded_accept_encoding.find_all { |m, q|
191
- q == 0.0
192
- }.each { |m, _|
193
- encoding_candidates.delete(m)
273
+ expanded_accept_encoding.each { |m, q|
274
+ encoding_candidates.delete(m) if q == 0.0
194
275
  }
195
276
 
196
277
  return (encoding_candidates & available_encodings)[0]
@@ -200,20 +281,53 @@ module Rack
200
281
  def set_cookie_header!(header, key, value)
201
282
  case value
202
283
  when Hash
203
- domain = "; domain=" + value[:domain] if value[:domain]
204
- path = "; path=" + value[:path] if value[:path]
205
- # According to RFC 2109, we need dashes here.
206
- # N.B.: cgi.rb uses spaces...
284
+ domain = "; domain=" + value[:domain] if value[:domain]
285
+ path = "; path=" + value[:path] if value[:path]
286
+ max_age = "; max-age=" + value[:max_age].to_s if value[:max_age]
287
+ # There is an RFC mess in the area of date formatting for Cookies. Not
288
+ # only are there contradicting RFCs and examples within RFC text, but
289
+ # there are also numerous conflicting names of fields and partially
290
+ # cross-applicable specifications.
291
+ #
292
+ # These are best described in RFC 2616 3.3.1. This RFC text also
293
+ # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
294
+ # fixed length format with space-date delimeted fields.
295
+ #
296
+ # See also RFC 1123 section 5.2.14.
297
+ #
298
+ # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
299
+ # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
300
+ # the space delimited format. These formats are compliant with RFC 2822.
301
+ #
302
+ # For reference, all involved RFCs are:
303
+ # RFC 822
304
+ # RFC 1123
305
+ # RFC 2109
306
+ # RFC 2616
307
+ # RFC 2822
308
+ # RFC 2965
309
+ # RFC 6265
207
310
  expires = "; expires=" +
208
311
  rfc2822(value[:expires].clone.gmtime) if value[:expires]
209
312
  secure = "; secure" if value[:secure]
210
- httponly = "; HttpOnly" if value[:httponly]
313
+ httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
314
+ same_site =
315
+ case value[:same_site]
316
+ when false, nil
317
+ nil
318
+ when :lax, 'Lax', :Lax
319
+ '; SameSite=Lax'.freeze
320
+ when true, :strict, 'Strict', :Strict
321
+ '; SameSite=Strict'.freeze
322
+ else
323
+ raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
324
+ end
211
325
  value = value[:value]
212
326
  end
213
327
  value = [value] unless Array === value
214
328
  cookie = escape(key) + "=" +
215
329
  value.map { |v| escape v }.join("&") +
216
- "#{domain}#{path}#{expires}#{secure}#{httponly}"
330
+ "#{domain}#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
217
331
 
218
332
  case header["Set-Cookie"]
219
333
  when nil, ''
@@ -241,6 +355,8 @@ module Rack
241
355
  cookies.reject! { |cookie|
242
356
  if value[:domain]
243
357
  cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
358
+ elsif value[:path]
359
+ cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
244
360
  else
245
361
  cookie =~ /\A#{escape(key)}=/
246
362
  end
@@ -250,42 +366,86 @@ module Rack
250
366
 
251
367
  set_cookie_header!(header, key,
252
368
  {:value => '', :path => nil, :domain => nil,
369
+ :max_age => '0',
253
370
  :expires => Time.at(0) }.merge(value))
254
371
 
255
372
  nil
256
373
  end
257
374
  module_function :delete_cookie_header!
258
375
 
376
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
377
+ # String#bytesize under 1.9.
378
+ if ''.respond_to?(:bytesize)
379
+ def bytesize(string)
380
+ string.bytesize
381
+ end
382
+ else
383
+ def bytesize(string)
384
+ string.size
385
+ end
386
+ end
387
+ module_function :bytesize
388
+
389
+ def rfc2822(time)
390
+ time.rfc2822
391
+ end
392
+ module_function :rfc2822
393
+
259
394
  # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
260
395
  # of '% %b %Y'.
261
396
  # It assumes that the time is in GMT to comply to the RFC 2109.
262
397
  #
263
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough
398
+ # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
264
399
  # that I'm certain someone implemented only that option.
265
400
  # Do not use %a and %b from Time.strptime, it would use localized names for
266
401
  # weekday and month.
267
402
  #
268
- def rfc2822(time)
403
+ def rfc2109(time)
269
404
  wday = Time::RFC2822_DAY_NAME[time.wday]
270
405
  mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
271
406
  time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
272
407
  end
273
- module_function :rfc2822
274
-
275
- # Return the bytesize of String; uses String#length under Ruby 1.8 and
276
- # String#bytesize under 1.9.
277
- if ''.respond_to?(:bytesize)
278
- def bytesize(string)
279
- string.bytesize
280
- end
281
- else
282
- def bytesize(string)
283
- string.size
408
+ module_function :rfc2109
409
+
410
+ # Parses the "Range:" header, if present, into an array of Range objects.
411
+ # Returns nil if the header is missing or syntactically invalid.
412
+ # Returns an empty array if none of the ranges are satisfiable.
413
+ def byte_ranges(env, size)
414
+ # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
415
+ http_range = env['HTTP_RANGE']
416
+ return nil unless http_range && http_range =~ /bytes=([^;]+)/
417
+ ranges = []
418
+ $1.split(/,\s*/).each do |range_spec|
419
+ return nil unless range_spec =~ /(\d*)-(\d*)/
420
+ r0,r1 = $1, $2
421
+ if r0.empty?
422
+ return nil if r1.empty?
423
+ # suffix-byte-range-spec, represents trailing suffix of file
424
+ r0 = size - r1.to_i
425
+ r0 = 0 if r0 < 0
426
+ r1 = size - 1
427
+ else
428
+ r0 = r0.to_i
429
+ if r1.empty?
430
+ r1 = size - 1
431
+ else
432
+ r1 = r1.to_i
433
+ return nil if r1 < r0 # backwards range is syntactically invalid
434
+ r1 = size-1 if r1 >= size
435
+ end
436
+ end
437
+ ranges << (r0..r1) if r0 <= r1
284
438
  end
439
+ ranges
285
440
  end
286
- module_function :bytesize
441
+ module_function :byte_ranges
287
442
 
288
443
  # Constant time string comparison.
444
+ #
445
+ # NOTE: the values compared should be of fixed length, such as strings
446
+ # that have already been processed by HMAC. This should not be used
447
+ # on variable length plaintext strings because it could leak length info
448
+ # via timing attacks.
289
449
  def secure_compare(a, b)
290
450
  return false unless bytesize(a) == bytesize(b)
291
451
 
@@ -343,23 +503,19 @@ module Rack
343
503
  end
344
504
 
345
505
  def to_hash
346
- inject({}) do |hash, (k,v)|
347
- if v.respond_to? :to_ary
348
- hash[k] = v.to_ary.join("\n")
349
- else
350
- hash[k] = v
351
- end
352
- hash
353
- end
506
+ hash = {}
507
+ each { |k,v| hash[k] = v }
508
+ hash
354
509
  end
355
510
 
356
511
  def [](k)
357
- super(@names[k] ||= @names[k.downcase])
512
+ super(k) || super(@names[k.downcase])
358
513
  end
359
514
 
360
515
  def []=(k, v)
361
- delete k
362
- @names[k] = @names[k.downcase] = k
516
+ canonical = k.downcase
517
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
518
+ @names[k] = @names[canonical] = k
363
519
  super k, v
364
520
  end
365
521
 
@@ -395,72 +551,116 @@ module Rack
395
551
  end
396
552
  end
397
553
 
554
+ class KeySpaceConstrainedParams
555
+ def initialize(limit = Utils.key_space_limit)
556
+ @limit = limit
557
+ @size = 0
558
+ @params = {}
559
+ end
560
+
561
+ def [](key)
562
+ @params[key]
563
+ end
564
+
565
+ def []=(key, value)
566
+ @size += key.size if key && !@params.key?(key)
567
+ raise RangeError, 'exceeded available parameter key space' if @size > @limit
568
+ @params[key] = value
569
+ end
570
+
571
+ def key?(key)
572
+ @params.key?(key)
573
+ end
574
+
575
+ def to_params_hash
576
+ hash = @params
577
+ hash.keys.each do |key|
578
+ value = hash[key]
579
+ if value.kind_of?(self.class)
580
+ if value.object_id == self.object_id
581
+ hash[key] = hash
582
+ else
583
+ hash[key] = value.to_params_hash
584
+ end
585
+ elsif value.kind_of?(Array)
586
+ value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
587
+ end
588
+ end
589
+ hash
590
+ end
591
+ end
592
+
398
593
  # Every standard HTTP code mapped to the appropriate message.
399
594
  # Generated with:
400
- # curl -s http://www.iana.org/assignments/http-status-codes | \
401
- # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
402
- # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
595
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
596
+ # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
597
+ # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
403
598
  HTTP_STATUS_CODES = {
404
- 100 => 'Continue',
405
- 101 => 'Switching Protocols',
406
- 102 => 'Processing',
407
- 200 => 'OK',
408
- 201 => 'Created',
409
- 202 => 'Accepted',
410
- 203 => 'Non-Authoritative Information',
411
- 204 => 'No Content',
412
- 205 => 'Reset Content',
413
- 206 => 'Partial Content',
414
- 207 => 'Multi-Status',
415
- 226 => 'IM Used',
416
- 300 => 'Multiple Choices',
417
- 301 => 'Moved Permanently',
418
- 302 => 'Found',
419
- 303 => 'See Other',
420
- 304 => 'Not Modified',
421
- 305 => 'Use Proxy',
422
- 306 => 'Reserved',
423
- 307 => 'Temporary Redirect',
424
- 400 => 'Bad Request',
425
- 401 => 'Unauthorized',
426
- 402 => 'Payment Required',
427
- 403 => 'Forbidden',
428
- 404 => 'Not Found',
429
- 405 => 'Method Not Allowed',
430
- 406 => 'Not Acceptable',
431
- 407 => 'Proxy Authentication Required',
432
- 408 => 'Request Timeout',
433
- 409 => 'Conflict',
434
- 410 => 'Gone',
435
- 411 => 'Length Required',
436
- 412 => 'Precondition Failed',
437
- 413 => 'Request Entity Too Large',
438
- 414 => 'Request-URI Too Long',
439
- 415 => 'Unsupported Media Type',
440
- 416 => 'Requested Range Not Satisfiable',
441
- 417 => 'Expectation Failed',
442
- 422 => 'Unprocessable Entity',
443
- 423 => 'Locked',
444
- 424 => 'Failed Dependency',
445
- 426 => 'Upgrade Required',
446
- 500 => 'Internal Server Error',
447
- 501 => 'Not Implemented',
448
- 502 => 'Bad Gateway',
449
- 503 => 'Service Unavailable',
450
- 504 => 'Gateway Timeout',
451
- 505 => 'HTTP Version Not Supported',
452
- 506 => 'Variant Also Negotiates',
453
- 507 => 'Insufficient Storage',
454
- 510 => 'Not Extended',
599
+ 100 => 'Continue',
600
+ 101 => 'Switching Protocols',
601
+ 102 => 'Processing',
602
+ 200 => 'OK',
603
+ 201 => 'Created',
604
+ 202 => 'Accepted',
605
+ 203 => 'Non-Authoritative Information',
606
+ 204 => 'No Content',
607
+ 205 => 'Reset Content',
608
+ 206 => 'Partial Content',
609
+ 207 => 'Multi-Status',
610
+ 208 => 'Already Reported',
611
+ 226 => 'IM Used',
612
+ 300 => 'Multiple Choices',
613
+ 301 => 'Moved Permanently',
614
+ 302 => 'Found',
615
+ 303 => 'See Other',
616
+ 304 => 'Not Modified',
617
+ 305 => 'Use Proxy',
618
+ 307 => 'Temporary Redirect',
619
+ 308 => 'Permanent Redirect',
620
+ 400 => 'Bad Request',
621
+ 401 => 'Unauthorized',
622
+ 402 => 'Payment Required',
623
+ 403 => 'Forbidden',
624
+ 404 => 'Not Found',
625
+ 405 => 'Method Not Allowed',
626
+ 406 => 'Not Acceptable',
627
+ 407 => 'Proxy Authentication Required',
628
+ 408 => 'Request Timeout',
629
+ 409 => 'Conflict',
630
+ 410 => 'Gone',
631
+ 411 => 'Length Required',
632
+ 412 => 'Precondition Failed',
633
+ 413 => 'Payload Too Large',
634
+ 414 => 'URI Too Long',
635
+ 415 => 'Unsupported Media Type',
636
+ 416 => 'Range Not Satisfiable',
637
+ 417 => 'Expectation Failed',
638
+ 422 => 'Unprocessable Entity',
639
+ 423 => 'Locked',
640
+ 424 => 'Failed Dependency',
641
+ 426 => 'Upgrade Required',
642
+ 428 => 'Precondition Required',
643
+ 429 => 'Too Many Requests',
644
+ 431 => 'Request Header Fields Too Large',
645
+ 500 => 'Internal Server Error',
646
+ 501 => 'Not Implemented',
647
+ 502 => 'Bad Gateway',
648
+ 503 => 'Service Unavailable',
649
+ 504 => 'Gateway Timeout',
650
+ 505 => 'HTTP Version Not Supported',
651
+ 506 => 'Variant Also Negotiates',
652
+ 507 => 'Insufficient Storage',
653
+ 508 => 'Loop Detected',
654
+ 510 => 'Not Extended',
655
+ 511 => 'Network Authentication Required'
455
656
  }
456
657
 
457
658
  # Responses with HTTP status codes that should not have an entity body
458
- STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
659
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304)
459
660
 
460
- SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
461
- hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
462
- hash
463
- }
661
+ SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
662
+ [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
663
+ }.flatten]
464
664
 
465
665
  def status_code(status)
466
666
  if status.is_a?(Symbol)
@@ -471,223 +671,25 @@ module Rack
471
671
  end
472
672
  module_function :status_code
473
673
 
474
- # A multipart form data parser, adapted from IOWA.
475
- #
476
- # Usually, Rack::Request#POST takes care of calling this.
477
-
478
- module Multipart
479
- class UploadedFile
480
- # The filename, *not* including the path, of the "uploaded" file
481
- attr_reader :original_filename
482
-
483
- # The content type of the "uploaded" file
484
- attr_accessor :content_type
485
-
486
- def initialize(path, content_type = "text/plain", binary = false)
487
- raise "#{path} file does not exist" unless ::File.exist?(path)
488
- @content_type = content_type
489
- @original_filename = ::File.basename(path)
490
- @tempfile = Tempfile.new(@original_filename)
491
- @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
492
- @tempfile.binmode if binary
493
- FileUtils.copy_file(path, @tempfile.path)
494
- end
495
-
496
- def path
497
- @tempfile.path
498
- end
499
- alias_method :local_path, :path
500
-
501
- def method_missing(method_name, *args, &block) #:nodoc:
502
- @tempfile.__send__(method_name, *args, &block)
503
- end
504
- end
505
-
506
- EOL = "\r\n"
507
- MULTIPART_BOUNDARY = "AaB03x"
508
-
509
- def self.parse_multipart(env)
510
- unless env['CONTENT_TYPE'] =~
511
- %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
512
- nil
513
- else
514
- boundary = "--#{$1}"
515
-
516
- params = {}
517
- buf = ""
518
- content_length = env['CONTENT_LENGTH'].to_i
519
- input = env['rack.input']
520
- input.rewind
521
-
522
- boundary_size = Utils.bytesize(boundary) + EOL.size
523
- bufsize = 16384
674
+ Multipart = Rack::Multipart
524
675
 
525
- content_length -= boundary_size
676
+ PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
526
677
 
527
- read_buffer = ''
678
+ def clean_path_info(path_info)
679
+ parts = path_info.split PATH_SEPS
528
680
 
529
- status = input.read(boundary_size, read_buffer)
530
- raise EOFError, "bad content body" unless status == boundary + EOL
681
+ clean = []
531
682
 
532
- rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
533
-
534
- max_key_space = Utils.key_space_limit
535
- bytes = 0
536
-
537
- loop {
538
- head = nil
539
- body = ''
540
- filename = content_type = name = nil
541
-
542
- until head && buf =~ rx
543
- if !head && i = buf.index(EOL+EOL)
544
- head = buf.slice!(0, i+2) # First \r\n
545
- buf.slice!(0, 2) # Second \r\n
546
-
547
- filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
548
- content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
549
- name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
550
-
551
- if name
552
- bytes += name.size
553
- if bytes > max_key_space
554
- raise RangeError, "exceeded available parameter key space"
555
- end
556
- end
557
-
558
- if content_type || filename
559
- body = Tempfile.new("RackMultipart")
560
- body.binmode if body.respond_to?(:binmode)
561
- end
562
-
563
- next
564
- end
565
-
566
- # Save the read body part.
567
- if head && (boundary_size+4 < buf.size)
568
- body << buf.slice!(0, buf.size - (boundary_size+4))
569
- end
570
-
571
- c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
572
- raise EOFError, "bad content body" if c.nil? || c.empty?
573
- buf << c
574
- content_length -= c.size
575
- end
576
-
577
- # Save the rest.
578
- if i = buf.index(rx)
579
- body << buf.slice!(0, i)
580
- buf.slice!(0, boundary_size+2)
581
-
582
- content_length = -1 if $1 == "--"
583
- end
584
-
585
- if filename == ""
586
- # filename is blank which means no file has been selected
587
- data = nil
588
- elsif filename
589
- body.rewind
590
-
591
- # Take the basename of the upload's original filename.
592
- # This handles the full Windows paths given by Internet Explorer
593
- # (and perhaps other broken user agents) without affecting
594
- # those which give the lone filename.
595
- filename =~ /^(?:.*[:\\\/])?(.*)/m
596
- filename = $1
597
-
598
- data = {:filename => filename, :type => content_type,
599
- :name => name, :tempfile => body, :head => head}
600
- elsif !filename && content_type
601
- body.rewind
602
-
603
- # Generic multipart cases, not coming from a form
604
- data = {:type => content_type,
605
- :name => name, :tempfile => body, :head => head}
606
- else
607
- data = body
608
- end
609
-
610
- Utils.normalize_params(params, name, data) unless data.nil?
611
-
612
- # break if we're at the end of a buffer, but not if it is the end of a field
613
- break if (buf.empty? && $1 != EOL) || content_length == -1
614
- }
615
-
616
- input.rewind
617
-
618
- params
619
- end
683
+ parts.each do |part|
684
+ next if part.empty? || part == '.'
685
+ part == '..' ? clean.pop : clean << part
620
686
  end
621
687
 
622
- def self.build_multipart(params, first = true)
623
- if first
624
- unless params.is_a?(Hash)
625
- raise ArgumentError, "value must be a Hash"
626
- end
688
+ clean.unshift '/' if parts.empty? || parts.first.empty?
627
689
 
628
- multipart = false
629
- query = lambda { |value|
630
- case value
631
- when Array
632
- value.each(&query)
633
- when Hash
634
- value.values.each(&query)
635
- when UploadedFile
636
- multipart = true
637
- end
638
- }
639
- params.values.each(&query)
640
- return nil unless multipart
641
- end
642
-
643
- flattened_params = Hash.new
644
-
645
- params.each do |key, value|
646
- k = first ? key.to_s : "[#{key}]"
647
-
648
- case value
649
- when Array
650
- value.map { |v|
651
- build_multipart(v, false).each { |subkey, subvalue|
652
- flattened_params["#{k}[]#{subkey}"] = subvalue
653
- }
654
- }
655
- when Hash
656
- build_multipart(value, false).each { |subkey, subvalue|
657
- flattened_params[k + subkey] = subvalue
658
- }
659
- else
660
- flattened_params[k] = value
661
- end
662
- end
663
-
664
- if first
665
- flattened_params.map { |name, file|
666
- if file.respond_to?(:original_filename)
667
- ::File.open(file.path, "rb") do |f|
668
- f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
669
- <<-EOF
670
- --#{MULTIPART_BOUNDARY}\r
671
- Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
672
- Content-Type: #{file.content_type}\r
673
- Content-Length: #{::File.stat(file.path).size}\r
674
- \r
675
- #{f.read}\r
676
- EOF
677
- end
678
- else
679
- <<-EOF
680
- --#{MULTIPART_BOUNDARY}\r
681
- Content-Disposition: form-data; name="#{name}"\r
682
- \r
683
- #{file}\r
684
- EOF
685
- end
686
- }.join + "--#{MULTIPART_BOUNDARY}--\r"
687
- else
688
- flattened_params
689
- end
690
- end
690
+ ::File.join(*clean)
691
691
  end
692
+ module_function :clean_path_info
693
+
692
694
  end
693
695
  end