rack 2.0.1 → 2.2.17
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +795 -0
- data/CONTRIBUTING.md +136 -0
- data/{COPYING → MIT-LICENSE} +4 -2
- data/README.rdoc +188 -145
- data/Rakefile +37 -23
- data/{SPEC → SPEC.rdoc} +46 -17
- data/bin/rackup +1 -0
- data/example/lobster.ru +2 -0
- data/example/protectedlobster.rb +3 -1
- data/example/protectedlobster.ru +2 -0
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +1 -1
- data/lib/rack/auth/basic.rb +6 -4
- data/lib/rack/auth/digest/md5.rb +13 -11
- data/lib/rack/auth/digest/nonce.rb +5 -3
- data/lib/rack/auth/digest/params.rb +4 -2
- data/lib/rack/auth/digest/request.rb +5 -3
- data/lib/rack/body_proxy.rb +15 -14
- data/lib/rack/builder.rb +116 -23
- data/lib/rack/cascade.rb +28 -12
- data/lib/rack/chunked.rb +68 -20
- data/lib/rack/common_logger.rb +37 -25
- data/lib/rack/conditional_get.rb +20 -16
- data/lib/rack/config.rb +2 -0
- data/lib/rack/content_length.rb +8 -7
- data/lib/rack/content_type.rb +5 -4
- data/lib/rack/core_ext/regexp.rb +14 -0
- data/lib/rack/deflater.rb +60 -70
- data/lib/rack/directory.rb +84 -64
- data/lib/rack/etag.rb +8 -5
- data/lib/rack/events.rb +19 -20
- data/lib/rack/file.rb +4 -173
- data/lib/rack/files.rb +218 -0
- data/lib/rack/handler/cgi.rb +2 -3
- data/lib/rack/handler/fastcgi.rb +4 -4
- data/lib/rack/handler/lsws.rb +3 -3
- data/lib/rack/handler/scgi.rb +9 -8
- data/lib/rack/handler/thin.rb +3 -3
- data/lib/rack/handler/webrick.rb +19 -10
- data/lib/rack/handler.rb +7 -2
- data/lib/rack/head.rb +1 -1
- data/lib/rack/lint.rb +221 -186
- data/lib/rack/lobster.rb +10 -10
- data/lib/rack/lock.rb +14 -4
- data/lib/rack/logger.rb +2 -0
- data/lib/rack/media_type.rb +23 -8
- data/lib/rack/method_override.rb +13 -4
- data/lib/rack/mime.rb +9 -1
- data/lib/rack/mock.rb +135 -29
- data/lib/rack/multipart/generator.rb +17 -13
- data/lib/rack/multipart/parser.rb +85 -68
- data/lib/rack/multipart/uploaded_file.rb +15 -7
- data/lib/rack/multipart.rb +6 -5
- data/lib/rack/null_logger.rb +2 -0
- data/lib/rack/query_parser.rb +108 -36
- data/lib/rack/recursive.rb +7 -5
- data/lib/rack/reloader.rb +8 -4
- data/lib/rack/request.rb +232 -60
- data/lib/rack/response.rb +127 -44
- data/lib/rack/rewindable_input.rb +4 -3
- data/lib/rack/runtime.rb +6 -4
- data/lib/rack/sendfile.rb +14 -10
- data/lib/rack/server.rb +97 -25
- data/lib/rack/session/abstract/id.rb +113 -25
- data/lib/rack/session/cookie.rb +22 -14
- data/lib/rack/session/memcache.rb +4 -87
- data/lib/rack/session/pool.rb +24 -10
- data/lib/rack/show_exceptions.rb +22 -18
- data/lib/rack/show_status.rb +9 -9
- data/lib/rack/static.rb +25 -12
- data/lib/rack/tempfile_reaper.rb +1 -1
- data/lib/rack/urlmap.rb +13 -7
- data/lib/rack/utils.rb +135 -123
- data/lib/rack/version.rb +29 -0
- data/lib/rack.rb +67 -73
- data/rack.gemspec +40 -29
- metadata +25 -184
- data/HISTORY.md +0 -505
- data/test/builder/an_underscore_app.rb +0 -5
- data/test/builder/anything.rb +0 -5
- data/test/builder/comment.ru +0 -4
- data/test/builder/end.ru +0 -5
- data/test/builder/line.ru +0 -1
- data/test/builder/options.ru +0 -2
- data/test/cgi/assets/folder/test.js +0 -1
- data/test/cgi/assets/fonts/font.eot +0 -1
- data/test/cgi/assets/images/image.png +0 -1
- data/test/cgi/assets/index.html +0 -1
- data/test/cgi/assets/javascripts/app.js +0 -1
- data/test/cgi/assets/stylesheets/app.css +0 -1
- data/test/cgi/lighttpd.conf +0 -26
- data/test/cgi/rackup_stub.rb +0 -6
- data/test/cgi/sample_rackup.ru +0 -5
- data/test/cgi/test +0 -9
- data/test/cgi/test+directory/test+file +0 -1
- data/test/cgi/test.fcgi +0 -9
- data/test/cgi/test.gz +0 -0
- data/test/cgi/test.ru +0 -5
- data/test/gemloader.rb +0 -10
- data/test/helper.rb +0 -34
- data/test/multipart/bad_robots +0 -259
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +0 -6
- data/test/multipart/empty +0 -10
- data/test/multipart/fail_16384_nofile +0 -814
- data/test/multipart/file1.txt +0 -1
- data/test/multipart/filename_and_modification_param +0 -7
- data/test/multipart/filename_and_no_name +0 -6
- data/test/multipart/filename_with_encoded_words +0 -7
- data/test/multipart/filename_with_escaped_quotes +0 -6
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
- data/test/multipart/filename_with_percent_escaped_quotes +0 -6
- data/test/multipart/filename_with_single_quote +0 -7
- data/test/multipart/filename_with_unescaped_percentages +0 -6
- data/test/multipart/filename_with_unescaped_percentages2 +0 -6
- data/test/multipart/filename_with_unescaped_percentages3 +0 -6
- data/test/multipart/filename_with_unescaped_quotes +0 -6
- data/test/multipart/ie +0 -6
- data/test/multipart/invalid_character +0 -6
- data/test/multipart/mixed_files +0 -21
- data/test/multipart/nested +0 -10
- data/test/multipart/none +0 -9
- data/test/multipart/quoted +0 -15
- data/test/multipart/rack-logo.png +0 -0
- data/test/multipart/semicolon +0 -6
- data/test/multipart/text +0 -15
- data/test/multipart/three_files_three_fields +0 -31
- data/test/multipart/unity3d_wwwform +0 -11
- data/test/multipart/webkit +0 -32
- data/test/rackup/config.ru +0 -31
- data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
- data/test/spec_auth_basic.rb +0 -89
- data/test/spec_auth_digest.rb +0 -260
- data/test/spec_body_proxy.rb +0 -85
- data/test/spec_builder.rb +0 -233
- data/test/spec_cascade.rb +0 -63
- data/test/spec_cgi.rb +0 -84
- data/test/spec_chunked.rb +0 -103
- data/test/spec_common_logger.rb +0 -95
- data/test/spec_conditional_get.rb +0 -103
- data/test/spec_config.rb +0 -23
- data/test/spec_content_length.rb +0 -86
- data/test/spec_content_type.rb +0 -46
- data/test/spec_deflater.rb +0 -365
- data/test/spec_directory.rb +0 -148
- data/test/spec_etag.rb +0 -108
- data/test/spec_events.rb +0 -133
- data/test/spec_fastcgi.rb +0 -85
- data/test/spec_file.rb +0 -251
- data/test/spec_handler.rb +0 -57
- data/test/spec_head.rb +0 -46
- data/test/spec_lint.rb +0 -515
- data/test/spec_lobster.rb +0 -59
- data/test/spec_lock.rb +0 -194
- data/test/spec_logger.rb +0 -24
- data/test/spec_media_type.rb +0 -42
- data/test/spec_method_override.rb +0 -83
- data/test/spec_mime.rb +0 -51
- data/test/spec_mock.rb +0 -342
- data/test/spec_multipart.rb +0 -716
- data/test/spec_null_logger.rb +0 -21
- data/test/spec_recursive.rb +0 -75
- data/test/spec_request.rb +0 -1393
- data/test/spec_response.rb +0 -510
- data/test/spec_rewindable_input.rb +0 -128
- data/test/spec_runtime.rb +0 -50
- data/test/spec_sendfile.rb +0 -125
- data/test/spec_server.rb +0 -193
- data/test/spec_session_abstract_id.rb +0 -31
- data/test/spec_session_abstract_session_hash.rb +0 -28
- data/test/spec_session_cookie.rb +0 -442
- data/test/spec_session_memcache.rb +0 -320
- data/test/spec_session_pool.rb +0 -210
- data/test/spec_show_exceptions.rb +0 -80
- data/test/spec_show_status.rb +0 -104
- data/test/spec_static.rb +0 -184
- data/test/spec_tempfile_reaper.rb +0 -64
- data/test/spec_thin.rb +0 -96
- data/test/spec_urlmap.rb +0 -237
- data/test/spec_utils.rb +0 -742
- data/test/spec_version.rb +0 -11
- data/test/spec_webrick.rb +0 -208
- data/test/static/another/index.html +0 -1
- data/test/static/foo.html +0 -1
- data/test/static/index.html +0 -1
- data/test/testrequest.rb +0 -78
- data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/lint.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'forwardable'
|
3
4
|
|
4
5
|
module Rack
|
@@ -33,26 +34,33 @@ module Rack
|
|
33
34
|
|
34
35
|
## A Rack application is a Ruby object (not a class) that
|
35
36
|
## responds to +call+.
|
36
|
-
def call(env=nil)
|
37
|
+
def call(env = nil)
|
37
38
|
dup._call(env)
|
38
39
|
end
|
39
40
|
|
40
41
|
def _call(env)
|
41
42
|
## It takes exactly one argument, the *environment*
|
42
|
-
|
43
|
+
raise LintError, "No env given" unless env
|
43
44
|
check_env env
|
44
45
|
|
45
46
|
env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT])
|
46
47
|
env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS])
|
47
48
|
|
48
49
|
## and returns an Array of exactly three values:
|
49
|
-
|
50
|
+
ary = @app.call(env)
|
51
|
+
raise LintError, "response is not an Array, but #{ary.class}" unless ary.kind_of? Array
|
52
|
+
raise LintError, "response array has #{ary.size} elements instead of 3" unless ary.size == 3
|
53
|
+
|
54
|
+
status, headers, @body = ary
|
50
55
|
## The *status*,
|
51
56
|
check_status status
|
52
57
|
## the *headers*,
|
53
58
|
check_headers headers
|
54
59
|
|
55
|
-
check_hijack_response headers, env
|
60
|
+
hijack_proc = check_hijack_response headers, env
|
61
|
+
if hijack_proc && headers.is_a?(Hash)
|
62
|
+
headers[RACK_HIJACK] = hijack_proc
|
63
|
+
end
|
56
64
|
|
57
65
|
## and the *body*.
|
58
66
|
check_content_type status, headers
|
@@ -63,12 +71,11 @@ module Rack
|
|
63
71
|
|
64
72
|
## == The Environment
|
65
73
|
def check_env(env)
|
66
|
-
## The environment must be an instance of Hash that includes
|
74
|
+
## The environment must be an unfrozen instance of Hash that includes
|
67
75
|
## CGI-like headers. The application is free to modify the
|
68
76
|
## environment.
|
69
|
-
|
70
|
-
|
71
|
-
}
|
77
|
+
raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash
|
78
|
+
raise LintError, "env should not be frozen, but is" if env.frozen?
|
72
79
|
|
73
80
|
##
|
74
81
|
## The environment is required to include these variables
|
@@ -102,17 +109,19 @@ module Rack
|
|
102
109
|
## follows the <tt>?</tt>, if any. May be
|
103
110
|
## empty, but is always required!
|
104
111
|
|
105
|
-
## <tt>SERVER_NAME</tt
|
106
|
-
## When combined with <tt>SCRIPT_NAME</tt> and
|
112
|
+
## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
|
107
113
|
## <tt>PATH_INFO</tt>, these variables can be
|
108
114
|
## used to complete the URL. Note, however,
|
109
115
|
## that <tt>HTTP_HOST</tt>, if present,
|
110
116
|
## should be used in preference to
|
111
117
|
## <tt>SERVER_NAME</tt> for reconstructing
|
112
118
|
## the request URL.
|
113
|
-
## <tt>SERVER_NAME</tt>
|
114
|
-
##
|
115
|
-
|
119
|
+
## <tt>SERVER_NAME</tt> can never be an empty
|
120
|
+
## string, and so is always required.
|
121
|
+
|
122
|
+
## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
|
123
|
+
## server is running on. Should be specified if
|
124
|
+
## the server is running on a non-standard port.
|
116
125
|
|
117
126
|
## <tt>HTTP_</tt> Variables:: Variables corresponding to the
|
118
127
|
## client-supplied HTTP request
|
@@ -123,9 +132,8 @@ module Rack
|
|
123
132
|
## the presence or absence of the
|
124
133
|
## appropriate HTTP header in the
|
125
134
|
## request. See
|
126
|
-
##
|
127
|
-
##
|
128
|
-
## specific behavior.
|
135
|
+
## {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
|
136
|
+
## for specific behavior.
|
129
137
|
|
130
138
|
## In addition to this, the Rack environment must include these
|
131
139
|
## Rack-specific variables:
|
@@ -179,68 +187,73 @@ module Rack
|
|
179
187
|
## The store must implement:
|
180
188
|
if session = env[RACK_SESSION]
|
181
189
|
## store(key, value) (aliased as []=);
|
182
|
-
|
183
|
-
session.
|
184
|
-
|
190
|
+
unless session.respond_to?(:store) && session.respond_to?(:[]=)
|
191
|
+
raise LintError, "session #{session.inspect} must respond to store and []="
|
192
|
+
end
|
185
193
|
|
186
194
|
## fetch(key, default = nil) (aliased as []);
|
187
|
-
|
188
|
-
session.
|
189
|
-
|
195
|
+
unless session.respond_to?(:fetch) && session.respond_to?(:[])
|
196
|
+
raise LintError, "session #{session.inspect} must respond to fetch and []"
|
197
|
+
end
|
190
198
|
|
191
199
|
## delete(key);
|
192
|
-
|
193
|
-
session.
|
194
|
-
|
200
|
+
unless session.respond_to?(:delete)
|
201
|
+
raise LintError, "session #{session.inspect} must respond to delete"
|
202
|
+
end
|
195
203
|
|
196
204
|
## clear;
|
197
|
-
|
198
|
-
session.
|
199
|
-
|
205
|
+
unless session.respond_to?(:clear)
|
206
|
+
raise LintError, "session #{session.inspect} must respond to clear"
|
207
|
+
end
|
208
|
+
|
209
|
+
## to_hash (returning unfrozen Hash instance);
|
210
|
+
unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
|
211
|
+
raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance"
|
212
|
+
end
|
200
213
|
end
|
201
214
|
|
202
215
|
## <tt>rack.logger</tt>:: A common object interface for logging messages.
|
203
216
|
## The object must implement:
|
204
217
|
if logger = env[RACK_LOGGER]
|
205
218
|
## info(message, &block)
|
206
|
-
|
207
|
-
logger.
|
208
|
-
|
219
|
+
unless logger.respond_to?(:info)
|
220
|
+
raise LintError, "logger #{logger.inspect} must respond to info"
|
221
|
+
end
|
209
222
|
|
210
223
|
## debug(message, &block)
|
211
|
-
|
212
|
-
logger.
|
213
|
-
|
224
|
+
unless logger.respond_to?(:debug)
|
225
|
+
raise LintError, "logger #{logger.inspect} must respond to debug"
|
226
|
+
end
|
214
227
|
|
215
228
|
## warn(message, &block)
|
216
|
-
|
217
|
-
logger.
|
218
|
-
|
229
|
+
unless logger.respond_to?(:warn)
|
230
|
+
raise LintError, "logger #{logger.inspect} must respond to warn"
|
231
|
+
end
|
219
232
|
|
220
233
|
## error(message, &block)
|
221
|
-
|
222
|
-
logger.
|
223
|
-
|
234
|
+
unless logger.respond_to?(:error)
|
235
|
+
raise LintError, "logger #{logger.inspect} must respond to error"
|
236
|
+
end
|
224
237
|
|
225
238
|
## fatal(message, &block)
|
226
|
-
|
227
|
-
logger.
|
228
|
-
|
239
|
+
unless logger.respond_to?(:fatal)
|
240
|
+
raise LintError, "logger #{logger.inspect} must respond to fatal"
|
241
|
+
end
|
229
242
|
end
|
230
243
|
|
231
244
|
## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
|
232
245
|
if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
|
233
|
-
|
234
|
-
|
235
|
-
|
246
|
+
unless bufsize.is_a?(Integer) && bufsize > 0
|
247
|
+
raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified"
|
248
|
+
end
|
236
249
|
end
|
237
250
|
|
238
251
|
## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
|
239
252
|
if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
|
240
|
-
|
253
|
+
raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call)
|
241
254
|
env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
|
242
255
|
io = tempfile_factory.call(filename, content_type)
|
243
|
-
|
256
|
+
raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<)
|
244
257
|
io
|
245
258
|
end
|
246
259
|
end
|
@@ -252,40 +265,61 @@ module Rack
|
|
252
265
|
## accepted specifications and must not be used otherwise.
|
253
266
|
##
|
254
267
|
|
255
|
-
%w[REQUEST_METHOD SERVER_NAME
|
256
|
-
QUERY_STRING
|
268
|
+
%w[REQUEST_METHOD SERVER_NAME QUERY_STRING
|
257
269
|
rack.version rack.input rack.errors
|
258
270
|
rack.multithread rack.multiprocess rack.run_once].each { |header|
|
259
|
-
|
271
|
+
raise LintError, "env missing required key #{header}" unless env.include? header
|
260
272
|
}
|
261
273
|
|
274
|
+
## The <tt>SERVER_PORT</tt> must be an Integer if set.
|
275
|
+
server_port = env["SERVER_PORT"]
|
276
|
+
unless server_port.nil? || (Integer(server_port) rescue false)
|
277
|
+
raise LintError, "env[SERVER_PORT] is not an Integer"
|
278
|
+
end
|
279
|
+
|
280
|
+
## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
|
281
|
+
unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false)
|
282
|
+
raise LintError, "#{env[SERVER_NAME]} must be a valid authority"
|
283
|
+
end
|
284
|
+
|
285
|
+
## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
|
286
|
+
unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false)
|
287
|
+
raise LintError, "#{env[HTTP_HOST]} must be a valid authority"
|
288
|
+
end
|
289
|
+
|
262
290
|
## The environment must not contain the keys
|
263
291
|
## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
|
264
292
|
## (use the versions without <tt>HTTP_</tt>).
|
265
293
|
%w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
|
266
|
-
|
267
|
-
|
268
|
-
|
294
|
+
if env.include? header
|
295
|
+
raise LintError, "env contains #{header}, must use #{header[5, -1]}"
|
296
|
+
end
|
269
297
|
}
|
270
298
|
|
271
299
|
## The CGI keys (named without a period) must have String values.
|
300
|
+
## If the string values for CGI keys contain non-ASCII characters,
|
301
|
+
## they should use ASCII-8BIT encoding.
|
272
302
|
env.each { |key, value|
|
273
303
|
next if key.include? "." # Skip extensions
|
274
|
-
|
275
|
-
value.
|
276
|
-
|
304
|
+
unless value.kind_of? String
|
305
|
+
raise LintError, "env variable #{key} has non-string value #{value.inspect}"
|
306
|
+
end
|
307
|
+
next if value.encoding == Encoding::ASCII_8BIT
|
308
|
+
unless value.b !~ /[\x80-\xff]/n
|
309
|
+
raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}"
|
310
|
+
end
|
277
311
|
}
|
278
312
|
|
279
313
|
## There are the following restrictions:
|
280
314
|
|
281
315
|
## * <tt>rack.version</tt> must be an array of Integers.
|
282
|
-
|
283
|
-
env[RACK_VERSION].
|
284
|
-
|
316
|
+
unless env[RACK_VERSION].kind_of? Array
|
317
|
+
raise LintError, "rack.version must be an Array, was #{env[RACK_VERSION].class}"
|
318
|
+
end
|
285
319
|
## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
|
286
|
-
|
287
|
-
|
288
|
-
|
320
|
+
unless %w[http https].include?(env[RACK_URL_SCHEME])
|
321
|
+
raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
|
322
|
+
end
|
289
323
|
|
290
324
|
## * There must be a valid input stream in <tt>rack.input</tt>.
|
291
325
|
check_input env[RACK_INPUT]
|
@@ -295,37 +329,33 @@ module Rack
|
|
295
329
|
check_hijack env
|
296
330
|
|
297
331
|
## * The <tt>REQUEST_METHOD</tt> must be a valid token.
|
298
|
-
|
299
|
-
|
300
|
-
|
332
|
+
unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
|
333
|
+
raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}"
|
334
|
+
end
|
301
335
|
|
302
336
|
## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
env[SCRIPT_NAME] =~ /\A\//
|
307
|
-
}
|
337
|
+
if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
|
338
|
+
raise LintError, "SCRIPT_NAME must start with /"
|
339
|
+
end
|
308
340
|
## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
env[PATH_INFO] =~ /\A\//
|
313
|
-
}
|
341
|
+
if env.include?(PATH_INFO) && env[PATH_INFO] != "" && env[PATH_INFO] !~ /\A\//
|
342
|
+
raise LintError, "PATH_INFO must start with /"
|
343
|
+
end
|
314
344
|
## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
|
315
|
-
|
316
|
-
|
317
|
-
|
345
|
+
if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
|
346
|
+
raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
|
347
|
+
end
|
318
348
|
|
319
349
|
## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
|
320
350
|
## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
|
321
351
|
## <tt>SCRIPT_NAME</tt> is empty.
|
322
|
-
|
323
|
-
|
324
|
-
|
352
|
+
unless env[SCRIPT_NAME] || env[PATH_INFO]
|
353
|
+
raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)"
|
354
|
+
end
|
325
355
|
## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
|
326
|
-
|
327
|
-
|
328
|
-
|
356
|
+
unless env[SCRIPT_NAME] != "/"
|
357
|
+
raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
|
358
|
+
end
|
329
359
|
end
|
330
360
|
|
331
361
|
## === The Input Stream
|
@@ -335,18 +365,18 @@ module Rack
|
|
335
365
|
def check_input(input)
|
336
366
|
## When applicable, its external encoding must be "ASCII-8BIT" and it
|
337
367
|
## must be opened in binary mode, for Ruby 1.9 compatibility.
|
338
|
-
|
339
|
-
input
|
340
|
-
|
341
|
-
|
342
|
-
input
|
343
|
-
|
368
|
+
if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
|
369
|
+
raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
|
370
|
+
end
|
371
|
+
if input.respond_to?(:binmode?) && !input.binmode?
|
372
|
+
raise LintError, "rack.input #{input} is not opened in binary mode"
|
373
|
+
end
|
344
374
|
|
345
375
|
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
|
346
376
|
[:gets, :each, :read, :rewind].each { |method|
|
347
|
-
|
348
|
-
input
|
349
|
-
|
377
|
+
unless input.respond_to? method
|
378
|
+
raise LintError, "rack.input #{input} does not respond to ##{method}"
|
379
|
+
end
|
350
380
|
}
|
351
381
|
end
|
352
382
|
|
@@ -360,11 +390,11 @@ module Rack
|
|
360
390
|
## * +gets+ must be called without arguments and return a string,
|
361
391
|
## or +nil+ on EOF.
|
362
392
|
def gets(*args)
|
363
|
-
|
393
|
+
raise LintError, "rack.input#gets called with arguments" unless args.size == 0
|
364
394
|
v = @input.gets
|
365
|
-
|
366
|
-
|
367
|
-
|
395
|
+
unless v.nil? or v.kind_of? String
|
396
|
+
raise LintError, "rack.input#gets didn't return a String"
|
397
|
+
end
|
368
398
|
v
|
369
399
|
end
|
370
400
|
|
@@ -386,32 +416,32 @@ module Rack
|
|
386
416
|
## If +buffer+ is given, then the read data will be placed
|
387
417
|
## into +buffer+ instead of a newly created String object.
|
388
418
|
def read(*args)
|
389
|
-
|
390
|
-
|
391
|
-
|
419
|
+
unless args.size <= 2
|
420
|
+
raise LintError, "rack.input#read called with too many arguments"
|
421
|
+
end
|
392
422
|
if args.size >= 1
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
423
|
+
unless args.first.kind_of?(Integer) || args.first.nil?
|
424
|
+
raise LintError, "rack.input#read called with non-integer and non-nil length"
|
425
|
+
end
|
426
|
+
unless args.first.nil? || args.first >= 0
|
427
|
+
raise LintError, "rack.input#read called with a negative length"
|
428
|
+
end
|
399
429
|
end
|
400
430
|
if args.size >= 2
|
401
|
-
|
402
|
-
|
403
|
-
|
431
|
+
unless args[1].kind_of?(String)
|
432
|
+
raise LintError, "rack.input#read called with non-String buffer"
|
433
|
+
end
|
404
434
|
end
|
405
435
|
|
406
436
|
v = @input.read(*args)
|
407
437
|
|
408
|
-
|
409
|
-
|
410
|
-
|
438
|
+
unless v.nil? or v.kind_of? String
|
439
|
+
raise LintError, "rack.input#read didn't return nil or a String"
|
440
|
+
end
|
411
441
|
if args[0].nil?
|
412
|
-
|
413
|
-
|
414
|
-
|
442
|
+
unless !v.nil?
|
443
|
+
raise LintError, "rack.input#read(nil) returned nil on EOF"
|
444
|
+
end
|
415
445
|
end
|
416
446
|
|
417
447
|
v
|
@@ -419,11 +449,11 @@ module Rack
|
|
419
449
|
|
420
450
|
## * +each+ must be called without arguments and only yield Strings.
|
421
451
|
def each(*args)
|
422
|
-
|
452
|
+
raise LintError, "rack.input#each called with arguments" unless args.size == 0
|
423
453
|
@input.each { |line|
|
424
|
-
|
425
|
-
|
426
|
-
|
454
|
+
unless line.kind_of? String
|
455
|
+
raise LintError, "rack.input#each didn't yield a String"
|
456
|
+
end
|
427
457
|
yield line
|
428
458
|
}
|
429
459
|
end
|
@@ -434,20 +464,18 @@ module Rack
|
|
434
464
|
## developers must buffer the input data into some rewindable object
|
435
465
|
## if the underlying input stream is not rewindable.
|
436
466
|
def rewind(*args)
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
end
|
445
|
-
}
|
467
|
+
raise LintError, "rack.input#rewind called with arguments" unless args.size == 0
|
468
|
+
begin
|
469
|
+
@input.rewind
|
470
|
+
true
|
471
|
+
rescue Errno::ESPIPE
|
472
|
+
raise LintError, "rack.input#rewind raised Errno::ESPIPE"
|
473
|
+
end
|
446
474
|
end
|
447
475
|
|
448
476
|
## * +close+ must never be called on the input stream.
|
449
477
|
def close(*args)
|
450
|
-
|
478
|
+
raise LintError, "rack.input#close must not be called"
|
451
479
|
end
|
452
480
|
end
|
453
481
|
|
@@ -455,9 +483,9 @@ module Rack
|
|
455
483
|
def check_error(error)
|
456
484
|
## The error stream must respond to +puts+, +write+ and +flush+.
|
457
485
|
[:puts, :write, :flush].each { |method|
|
458
|
-
|
459
|
-
error
|
460
|
-
|
486
|
+
unless error.respond_to? method
|
487
|
+
raise LintError, "rack.error #{error} does not respond to ##{method}"
|
488
|
+
end
|
461
489
|
}
|
462
490
|
end
|
463
491
|
|
@@ -475,7 +503,7 @@ module Rack
|
|
475
503
|
|
476
504
|
## * +write+ must be called with a single argument that is a String.
|
477
505
|
def write(str)
|
478
|
-
|
506
|
+
raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String
|
479
507
|
@error.write str
|
480
508
|
end
|
481
509
|
|
@@ -487,7 +515,7 @@ module Rack
|
|
487
515
|
|
488
516
|
## * +close+ must never be called on the error stream.
|
489
517
|
def close(*args)
|
490
|
-
|
518
|
+
raise LintError, "rack.errors#close must not be called"
|
491
519
|
end
|
492
520
|
end
|
493
521
|
|
@@ -505,7 +533,7 @@ module Rack
|
|
505
533
|
def initialize(io)
|
506
534
|
@io = io
|
507
535
|
REQUIRED_METHODS.each do |meth|
|
508
|
-
|
536
|
+
raise LintError, "rack.hijack_io must respond to #{meth}" unless io.respond_to? meth
|
509
537
|
end
|
510
538
|
end
|
511
539
|
end
|
@@ -521,7 +549,7 @@ module Rack
|
|
521
549
|
if env[RACK_IS_HIJACK]
|
522
550
|
## If rack.hijack? is true then rack.hijack must respond to #call.
|
523
551
|
original_hijack = env[RACK_HIJACK]
|
524
|
-
|
552
|
+
raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call)
|
525
553
|
env[RACK_HIJACK] = proc do
|
526
554
|
## rack.hijack must return the io that will also be assigned (or is
|
527
555
|
## already present, in rack.hijack_io.
|
@@ -554,10 +582,10 @@ module Rack
|
|
554
582
|
else
|
555
583
|
##
|
556
584
|
## If rack.hijack? is false, then rack.hijack should not be set.
|
557
|
-
|
585
|
+
raise LintError, "rack.hijack? is false, but rack.hijack is present" unless env[RACK_HIJACK].nil?
|
558
586
|
##
|
559
587
|
## If rack.hijack? is false, then rack.hijack_io should not be set.
|
560
|
-
|
588
|
+
raise LintError, "rack.hijack? is false, but rack.hijack_io is present" unless env[RACK_HIJACK_IO].nil?
|
561
589
|
end
|
562
590
|
end
|
563
591
|
|
@@ -568,7 +596,7 @@ module Rack
|
|
568
596
|
|
569
597
|
# this check uses headers like a hash, but the spec only requires
|
570
598
|
# headers respond to #each
|
571
|
-
headers = Rack::Utils::HeaderHash
|
599
|
+
headers = Rack::Utils::HeaderHash[headers]
|
572
600
|
|
573
601
|
## In order to do this, an application may set the special header
|
574
602
|
## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
|
@@ -588,20 +616,22 @@ module Rack
|
|
588
616
|
## the <tt>rack.hijack</tt> response API is in use.
|
589
617
|
|
590
618
|
if env[RACK_IS_HIJACK] && headers[RACK_HIJACK]
|
591
|
-
|
592
|
-
|
593
|
-
|
619
|
+
unless headers[RACK_HIJACK].respond_to? :call
|
620
|
+
raise LintError, 'rack.hijack header must respond to #call'
|
621
|
+
end
|
594
622
|
original_hijack = headers[RACK_HIJACK]
|
595
|
-
|
623
|
+
proc do |io|
|
596
624
|
original_hijack.call HijackWrapper.new(io)
|
597
625
|
end
|
598
626
|
else
|
599
627
|
##
|
600
628
|
## The special response header <tt>rack.hijack</tt> must only be set
|
601
629
|
## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
|
602
|
-
|
603
|
-
|
604
|
-
|
630
|
+
unless headers[RACK_HIJACK].nil?
|
631
|
+
raise LintError, 'rack.hijack header must not be present if server does not support hijacking'
|
632
|
+
end
|
633
|
+
|
634
|
+
nil
|
605
635
|
end
|
606
636
|
end
|
607
637
|
## ==== Conventions
|
@@ -617,40 +647,45 @@ module Rack
|
|
617
647
|
def check_status(status)
|
618
648
|
## This is an HTTP status. When parsed as integer (+to_i+), it must be
|
619
649
|
## greater than or equal to 100.
|
620
|
-
|
650
|
+
unless status.to_i >= 100
|
651
|
+
raise LintError, "Status must be >=100 seen as integer"
|
652
|
+
end
|
621
653
|
end
|
622
654
|
|
623
655
|
## === The Headers
|
624
656
|
def check_headers(header)
|
625
657
|
## The header must respond to +each+, and yield values of key and value.
|
626
|
-
|
627
|
-
|
628
|
-
|
658
|
+
unless header.respond_to? :each
|
659
|
+
raise LintError, "headers object should respond to #each, but doesn't (got #{header.class} as headers)"
|
660
|
+
end
|
661
|
+
|
629
662
|
header.each { |key, value|
|
663
|
+
## The header keys must be Strings.
|
664
|
+
unless key.kind_of? String
|
665
|
+
raise LintError, "header key must be a string, was #{key.class}"
|
666
|
+
end
|
667
|
+
|
630
668
|
## Special headers starting "rack." are for communicating with the
|
631
669
|
## server, and must not be sent back to the client.
|
632
670
|
next if key =~ /^rack\..+$/
|
633
671
|
|
634
|
-
## The header keys must be Strings.
|
635
|
-
assert("header key must be a string, was #{key.class}") {
|
636
|
-
key.kind_of? String
|
637
|
-
}
|
638
672
|
## The header must not contain a +Status+ key.
|
639
|
-
|
673
|
+
raise LintError, "header must not contain Status" if key.downcase == "status"
|
640
674
|
## The header must conform to RFC7230 token specification, i.e. cannot
|
641
675
|
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
|
642
|
-
|
676
|
+
raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/
|
643
677
|
|
644
678
|
## The values of the header must be Strings,
|
645
|
-
|
646
|
-
"'#{key}' is a #{value.class}"
|
679
|
+
unless value.kind_of? String
|
680
|
+
raise LintError, "a header value must be a String, but the value of '#{key}' is a #{value.class}"
|
681
|
+
end
|
647
682
|
## consisting of lines (for multiple header values, e.g. multiple
|
648
683
|
## <tt>Set-Cookie</tt> values) separated by "\\n".
|
649
684
|
value.split("\n").each { |item|
|
650
685
|
## The lines must not contain characters below 037.
|
651
|
-
|
652
|
-
|
653
|
-
|
686
|
+
if item =~ /[\000-\037]/
|
687
|
+
raise LintError, "invalid header value #{key}: #{item.inspect}"
|
688
|
+
end
|
654
689
|
}
|
655
690
|
}
|
656
691
|
end
|
@@ -659,11 +694,11 @@ module Rack
|
|
659
694
|
def check_content_type(status, headers)
|
660
695
|
headers.each { |key, value|
|
661
696
|
## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
|
662
|
-
## 204
|
697
|
+
## 204 or 304.
|
663
698
|
if key.downcase == "content-type"
|
664
|
-
|
665
|
-
|
666
|
-
|
699
|
+
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
|
700
|
+
raise LintError, "Content-Type header found in #{status} response, not allowed"
|
701
|
+
end
|
667
702
|
return
|
668
703
|
end
|
669
704
|
}
|
@@ -674,10 +709,10 @@ module Rack
|
|
674
709
|
headers.each { |key, value|
|
675
710
|
if key.downcase == 'content-length'
|
676
711
|
## There must not be a <tt>Content-Length</tt> header when the
|
677
|
-
## +Status+ is 1xx, 204
|
678
|
-
|
679
|
-
|
680
|
-
|
712
|
+
## +Status+ is 1xx, 204 or 304.
|
713
|
+
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
|
714
|
+
raise LintError, "Content-Length header found in #{status} response, not allowed"
|
715
|
+
end
|
681
716
|
@content_length = value
|
682
717
|
end
|
683
718
|
}
|
@@ -685,13 +720,13 @@ module Rack
|
|
685
720
|
|
686
721
|
def verify_content_length(bytes)
|
687
722
|
if @head_request
|
688
|
-
|
689
|
-
|
690
|
-
|
723
|
+
unless bytes == 0
|
724
|
+
raise LintError, "Response body was given for HEAD request, but should be empty"
|
725
|
+
end
|
691
726
|
elsif @content_length
|
692
|
-
|
693
|
-
@content_length
|
694
|
-
|
727
|
+
unless @content_length == bytes.to_s
|
728
|
+
raise LintError, "Content-Length header was #{@content_length}, but should be #{bytes}"
|
729
|
+
end
|
695
730
|
end
|
696
731
|
end
|
697
732
|
|
@@ -701,15 +736,15 @@ module Rack
|
|
701
736
|
bytes = 0
|
702
737
|
|
703
738
|
## The Body must respond to +each+
|
704
|
-
|
705
|
-
|
739
|
+
unless @body.respond_to?(:each)
|
740
|
+
raise LintError, "Response body must respond to each"
|
706
741
|
end
|
707
742
|
|
708
743
|
@body.each { |part|
|
709
744
|
## and must only yield String values.
|
710
|
-
|
711
|
-
part.
|
712
|
-
|
745
|
+
unless part.kind_of? String
|
746
|
+
raise LintError, "Body yielded non-string value #{part.inspect}"
|
747
|
+
end
|
713
748
|
bytes += part.bytesize
|
714
749
|
yield part
|
715
750
|
}
|
@@ -722,7 +757,7 @@ module Rack
|
|
722
757
|
## If the Body responds to +close+, it will be called after iteration. If
|
723
758
|
## the body is replaced by a middleware after action, the original body
|
724
759
|
## must be closed first, if it responds to close.
|
725
|
-
# XXX howto:
|
760
|
+
# XXX howto: raise LintError, "Body has not been closed" unless @closed
|
726
761
|
|
727
762
|
|
728
763
|
##
|
@@ -733,9 +768,9 @@ module Rack
|
|
733
768
|
## transport the response.
|
734
769
|
|
735
770
|
if @body.respond_to?(:to_path)
|
736
|
-
|
737
|
-
|
738
|
-
|
771
|
+
unless ::File.exist? @body.to_path
|
772
|
+
raise LintError, "The file identified by body.to_path does not exist"
|
773
|
+
end
|
739
774
|
end
|
740
775
|
|
741
776
|
##
|