rack 2.1.0 → 2.2.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -6
- data/CONTRIBUTING.md +136 -0
- data/README.rdoc +83 -39
- data/Rakefile +14 -7
- data/{SPEC → SPEC.rdoc} +26 -1
- data/lib/rack.rb +7 -16
- data/lib/rack/auth/abstract/request.rb +0 -2
- data/lib/rack/auth/basic.rb +3 -3
- data/lib/rack/auth/digest/md5.rb +4 -4
- data/lib/rack/auth/digest/request.rb +3 -3
- data/lib/rack/body_proxy.rb +13 -9
- data/lib/rack/builder.rb +78 -8
- data/lib/rack/cascade.rb +23 -8
- data/lib/rack/chunked.rb +48 -23
- data/lib/rack/common_logger.rb +25 -18
- data/lib/rack/conditional_get.rb +18 -16
- data/lib/rack/content_length.rb +6 -7
- data/lib/rack/content_type.rb +3 -4
- data/lib/rack/deflater.rb +49 -35
- data/lib/rack/directory.rb +77 -60
- data/lib/rack/etag.rb +2 -3
- data/lib/rack/events.rb +15 -18
- data/lib/rack/file.rb +1 -2
- data/lib/rack/files.rb +97 -57
- data/lib/rack/handler/cgi.rb +1 -4
- data/lib/rack/handler/fastcgi.rb +1 -3
- data/lib/rack/handler/lsws.rb +1 -3
- data/lib/rack/handler/scgi.rb +1 -3
- data/lib/rack/handler/thin.rb +1 -3
- data/lib/rack/handler/webrick.rb +12 -5
- data/lib/rack/head.rb +0 -2
- data/lib/rack/lint.rb +57 -14
- data/lib/rack/lobster.rb +3 -5
- data/lib/rack/lock.rb +0 -1
- data/lib/rack/mock.rb +22 -4
- data/lib/rack/multipart.rb +1 -1
- data/lib/rack/multipart/generator.rb +11 -6
- data/lib/rack/multipart/parser.rb +10 -18
- data/lib/rack/multipart/uploaded_file.rb +13 -7
- data/lib/rack/query_parser.rb +7 -8
- data/lib/rack/recursive.rb +1 -1
- data/lib/rack/reloader.rb +1 -3
- data/lib/rack/request.rb +182 -76
- data/lib/rack/response.rb +62 -19
- data/lib/rack/rewindable_input.rb +0 -1
- data/lib/rack/runtime.rb +3 -3
- data/lib/rack/sendfile.rb +0 -3
- data/lib/rack/server.rb +9 -10
- data/lib/rack/session/abstract/id.rb +23 -28
- data/lib/rack/session/cookie.rb +1 -3
- data/lib/rack/session/pool.rb +1 -1
- data/lib/rack/show_exceptions.rb +6 -8
- data/lib/rack/show_status.rb +5 -7
- data/lib/rack/static.rb +13 -6
- data/lib/rack/tempfile_reaper.rb +0 -2
- data/lib/rack/urlmap.rb +1 -4
- data/lib/rack/utils.rb +58 -54
- data/lib/rack/version.rb +29 -0
- data/rack.gemspec +31 -29
- metadata +11 -12
data/lib/rack/show_status.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'erb'
|
4
|
-
require 'rack/request'
|
5
|
-
require 'rack/utils'
|
6
4
|
|
7
5
|
module Rack
|
8
6
|
# Rack::ShowStatus catches all empty responses and replaces them
|
@@ -20,19 +18,19 @@ module Rack
|
|
20
18
|
|
21
19
|
def call(env)
|
22
20
|
status, headers, body = @app.call(env)
|
23
|
-
headers = Utils::HeaderHash
|
21
|
+
headers = Utils::HeaderHash[headers]
|
24
22
|
empty = headers[CONTENT_LENGTH].to_i <= 0
|
25
23
|
|
26
24
|
# client or server error, or explicit message
|
27
25
|
if (status.to_i >= 400 && empty) || env[RACK_SHOWSTATUS_DETAIL]
|
28
|
-
# This double assignment is to prevent an "unused variable" warning
|
29
|
-
#
|
26
|
+
# This double assignment is to prevent an "unused variable" warning.
|
27
|
+
# Yes, it is dumb, but I don't like Ruby yelling at me.
|
30
28
|
req = req = Rack::Request.new(env)
|
31
29
|
|
32
30
|
message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
|
33
31
|
|
34
|
-
# This double assignment is to prevent an "unused variable" warning
|
35
|
-
#
|
32
|
+
# This double assignment is to prevent an "unused variable" warning.
|
33
|
+
# Yes, it is dumb, but I don't like Ruby yelling at me.
|
36
34
|
detail = detail = env[RACK_SHOWSTATUS_DETAIL] || message
|
37
35
|
|
38
36
|
body = @template.result(binding)
|
data/lib/rack/static.rb
CHANGED
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "rack/files"
|
4
|
-
require "rack/utils"
|
5
|
-
|
6
|
-
require_relative 'core_ext/regexp'
|
7
|
-
|
8
3
|
module Rack
|
9
4
|
|
10
5
|
# The Rack::Static middleware intercepts requests for static files
|
@@ -19,6 +14,11 @@ module Rack
|
|
19
14
|
#
|
20
15
|
# use Rack::Static, :urls => ["/media"]
|
21
16
|
#
|
17
|
+
# Same as previous, but instead of returning 404 for missing files under
|
18
|
+
# /media, call the next middleware:
|
19
|
+
#
|
20
|
+
# use Rack::Static, :urls => ["/media"], :cascade => true
|
21
|
+
#
|
22
22
|
# Serve all requests beginning with /css or /images from the folder "public"
|
23
23
|
# in the current directory (ie public/css/* and public/images/*):
|
24
24
|
#
|
@@ -86,13 +86,14 @@ module Rack
|
|
86
86
|
# ]
|
87
87
|
#
|
88
88
|
class Static
|
89
|
-
using ::Rack::RegexpExtensions
|
89
|
+
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
|
90
90
|
|
91
91
|
def initialize(app, options = {})
|
92
92
|
@app = app
|
93
93
|
@urls = options[:urls] || ["/favicon.ico"]
|
94
94
|
@index = options[:index]
|
95
95
|
@gzip = options[:gzip]
|
96
|
+
@cascade = options[:cascade]
|
96
97
|
root = options[:root] || Dir.pwd
|
97
98
|
|
98
99
|
# HTTP Headers
|
@@ -133,6 +134,8 @@ module Rack
|
|
133
134
|
|
134
135
|
if response[0] == 404
|
135
136
|
response = nil
|
137
|
+
elsif response[0] == 304
|
138
|
+
# Do nothing, leave headers as is
|
136
139
|
else
|
137
140
|
if mime_type = Mime.mime_type(::File.extname(path), 'text/plain')
|
138
141
|
response[1][CONTENT_TYPE] = mime_type
|
@@ -144,6 +147,10 @@ module Rack
|
|
144
147
|
path = env[PATH_INFO]
|
145
148
|
response ||= @file_server.call(env)
|
146
149
|
|
150
|
+
if @cascade && response[0] == 404
|
151
|
+
return @app.call(env)
|
152
|
+
end
|
153
|
+
|
147
154
|
headers = response[1]
|
148
155
|
applicable_rules(path).each do |rule, new_headers|
|
149
156
|
new_headers.each { |field, content| headers[field] = content }
|
data/lib/rack/tempfile_reaper.rb
CHANGED
data/lib/rack/urlmap.rb
CHANGED
@@ -16,9 +16,6 @@ module Rack
|
|
16
16
|
# first, since they are most specific.
|
17
17
|
|
18
18
|
class URLMap
|
19
|
-
NEGATIVE_INFINITY = -1.0 / 0.0
|
20
|
-
INFINITY = 1.0 / 0.0
|
21
|
-
|
22
19
|
def initialize(map = {})
|
23
20
|
remap(map)
|
24
21
|
end
|
@@ -42,7 +39,7 @@ module Rack
|
|
42
39
|
|
43
40
|
[host, location, match, app]
|
44
41
|
}.sort_by do |(host, location, _, _)|
|
45
|
-
[host ? -host.size : INFINITY, -location.size]
|
42
|
+
[host ? -host.size : Float::INFINITY, -location.size]
|
46
43
|
end
|
47
44
|
end
|
48
45
|
|
data/lib/rack/utils.rb
CHANGED
@@ -5,17 +5,16 @@ require 'uri'
|
|
5
5
|
require 'fileutils'
|
6
6
|
require 'set'
|
7
7
|
require 'tempfile'
|
8
|
-
require 'rack/query_parser'
|
9
8
|
require 'time'
|
10
9
|
|
11
|
-
require_relative '
|
10
|
+
require_relative 'query_parser'
|
12
11
|
|
13
12
|
module Rack
|
14
13
|
# Rack::Utils contains a grab-bag of useful methods for writing web
|
15
14
|
# applications adopted from all kinds of Ruby libraries.
|
16
15
|
|
17
16
|
module Utils
|
18
|
-
using ::Rack::RegexpExtensions
|
17
|
+
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
|
19
18
|
|
20
19
|
ParameterTypeError = QueryParser::ParameterTypeError
|
21
20
|
InvalidParameterError = QueryParser::InvalidParameterError
|
@@ -30,33 +29,30 @@ module Rack
|
|
30
29
|
# This helps prevent a rogue client from flooding a Request.
|
31
30
|
self.default_query_parser = QueryParser.make_default(65536, 100)
|
32
31
|
|
32
|
+
module_function
|
33
|
+
|
33
34
|
# URI escapes. (CGI style space to +)
|
34
35
|
def escape(s)
|
35
36
|
URI.encode_www_form_component(s)
|
36
37
|
end
|
37
|
-
module_function :escape
|
38
38
|
|
39
39
|
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
40
40
|
# true URI escaping.
|
41
41
|
def escape_path(s)
|
42
42
|
::URI::DEFAULT_PARSER.escape s
|
43
43
|
end
|
44
|
-
module_function :escape_path
|
45
44
|
|
46
45
|
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
47
46
|
# unescaping query parameters or form components.
|
48
47
|
def unescape_path(s)
|
49
48
|
::URI::DEFAULT_PARSER.unescape s
|
50
49
|
end
|
51
|
-
module_function :unescape_path
|
52
|
-
|
53
50
|
|
54
51
|
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
55
52
|
# target encoding of the string returned, and it defaults to UTF-8
|
56
53
|
def unescape(s, encoding = Encoding::UTF_8)
|
57
54
|
URI.decode_www_form_component(s, encoding)
|
58
55
|
end
|
59
|
-
module_function :unescape
|
60
56
|
|
61
57
|
class << self
|
62
58
|
attr_accessor :multipart_part_limit
|
@@ -88,21 +84,20 @@ module Rack
|
|
88
84
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
89
85
|
end
|
90
86
|
else
|
87
|
+
# :nocov:
|
91
88
|
def clock_time
|
92
89
|
Time.now.to_f
|
93
90
|
end
|
91
|
+
# :nocov:
|
94
92
|
end
|
95
|
-
module_function :clock_time
|
96
93
|
|
97
94
|
def parse_query(qs, d = nil, &unescaper)
|
98
95
|
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
|
99
96
|
end
|
100
|
-
module_function :parse_query
|
101
97
|
|
102
98
|
def parse_nested_query(qs, d = nil)
|
103
99
|
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
|
104
100
|
end
|
105
|
-
module_function :parse_nested_query
|
106
101
|
|
107
102
|
def build_query(params)
|
108
103
|
params.map { |k, v|
|
@@ -113,7 +108,6 @@ module Rack
|
|
113
108
|
end
|
114
109
|
}.join("&")
|
115
110
|
end
|
116
|
-
module_function :build_query
|
117
111
|
|
118
112
|
def build_nested_query(value, prefix = nil)
|
119
113
|
case value
|
@@ -132,7 +126,6 @@ module Rack
|
|
132
126
|
"#{prefix}=#{escape(value)}"
|
133
127
|
end
|
134
128
|
end
|
135
|
-
module_function :build_nested_query
|
136
129
|
|
137
130
|
def q_values(q_value_header)
|
138
131
|
q_value_header.to_s.split(/\s*,\s*/).map do |part|
|
@@ -144,8 +137,11 @@ module Rack
|
|
144
137
|
[value, quality]
|
145
138
|
end
|
146
139
|
end
|
147
|
-
module_function :q_values
|
148
140
|
|
141
|
+
# Return best accept value to use, based on the algorithm
|
142
|
+
# in RFC 2616 Section 14. If there are multiple best
|
143
|
+
# matches (same specificity and quality), the value returned
|
144
|
+
# is arbitrary.
|
149
145
|
def best_q_match(q_value_header, available_mimes)
|
150
146
|
values = q_values(q_value_header)
|
151
147
|
|
@@ -158,7 +154,6 @@ module Rack
|
|
158
154
|
end.last
|
159
155
|
matches && matches.first
|
160
156
|
end
|
161
|
-
module_function :best_q_match
|
162
157
|
|
163
158
|
ESCAPE_HTML = {
|
164
159
|
"&" => "&",
|
@@ -175,22 +170,27 @@ module Rack
|
|
175
170
|
def escape_html(string)
|
176
171
|
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
177
172
|
end
|
178
|
-
module_function :escape_html
|
179
173
|
|
180
174
|
def select_best_encoding(available_encodings, accept_encoding)
|
181
175
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
182
176
|
|
183
|
-
expanded_accept_encoding =
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
177
|
+
expanded_accept_encoding = []
|
178
|
+
|
179
|
+
accept_encoding.each do |m, q|
|
180
|
+
preference = available_encodings.index(m) || available_encodings.size
|
181
|
+
|
182
|
+
if m == "*"
|
183
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
184
|
+
expanded_accept_encoding << [m2, q, preference]
|
190
185
|
end
|
186
|
+
else
|
187
|
+
expanded_accept_encoding << [m, q, preference]
|
191
188
|
end
|
189
|
+
end
|
192
190
|
|
193
|
-
encoding_candidates = expanded_accept_encoding
|
191
|
+
encoding_candidates = expanded_accept_encoding
|
192
|
+
.sort_by { |_, q, p| [-q, p] }
|
193
|
+
.map!(&:first)
|
194
194
|
|
195
195
|
unless encoding_candidates.include?("identity")
|
196
196
|
encoding_candidates.push("identity")
|
@@ -202,23 +202,19 @@ module Rack
|
|
202
202
|
|
203
203
|
(encoding_candidates & available_encodings)[0]
|
204
204
|
end
|
205
|
-
module_function :select_best_encoding
|
206
205
|
|
207
206
|
def parse_cookies(env)
|
208
207
|
parse_cookies_header env[HTTP_COOKIE]
|
209
208
|
end
|
210
|
-
module_function :parse_cookies
|
211
209
|
|
212
210
|
def parse_cookies_header(header)
|
213
|
-
# According to RFC
|
214
|
-
#
|
215
|
-
#
|
216
|
-
#
|
217
|
-
|
218
|
-
cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
|
211
|
+
# According to RFC 6265:
|
212
|
+
# The syntax for cookie headers only supports semicolons
|
213
|
+
# User Agent -> Server ==
|
214
|
+
# Cookie: SID=31d4d96e407aad42; lang=en-US
|
215
|
+
cookies = parse_query(header, ';') { |s| unescape(s) rescue s }
|
219
216
|
cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v }
|
220
217
|
end
|
221
|
-
module_function :parse_cookies_header
|
222
218
|
|
223
219
|
def add_cookie_to_header(header, key, value)
|
224
220
|
case value
|
@@ -260,13 +256,11 @@ module Rack
|
|
260
256
|
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
|
261
257
|
end
|
262
258
|
end
|
263
|
-
module_function :add_cookie_to_header
|
264
259
|
|
265
260
|
def set_cookie_header!(header, key, value)
|
266
261
|
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
|
267
262
|
nil
|
268
263
|
end
|
269
|
-
module_function :set_cookie_header!
|
270
264
|
|
271
265
|
def make_delete_cookie_header(header, key, value)
|
272
266
|
case header
|
@@ -278,25 +272,30 @@ module Rack
|
|
278
272
|
cookies = header
|
279
273
|
end
|
280
274
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
275
|
+
key = escape(key)
|
276
|
+
domain = value[:domain]
|
277
|
+
path = value[:path]
|
278
|
+
regexp = if domain
|
279
|
+
if path
|
280
|
+
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
|
281
|
+
else
|
282
|
+
/\A#{key}=.*domain=#{domain}(?:;|$)/
|
283
|
+
end
|
284
|
+
elsif path
|
285
|
+
/\A#{key}=.*path=#{path}(?:;|$)/
|
285
286
|
else
|
286
|
-
/\A#{
|
287
|
+
/\A#{key}=/
|
287
288
|
end
|
288
289
|
|
289
290
|
cookies.reject! { |cookie| regexp.match? cookie }
|
290
291
|
|
291
292
|
cookies.join("\n")
|
292
293
|
end
|
293
|
-
module_function :make_delete_cookie_header
|
294
294
|
|
295
295
|
def delete_cookie_header!(header, key, value = {})
|
296
296
|
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
|
297
297
|
nil
|
298
298
|
end
|
299
|
-
module_function :delete_cookie_header!
|
300
299
|
|
301
300
|
# Adds a cookie that will *remove* a cookie from the client. Hence the
|
302
301
|
# strange method name.
|
@@ -309,12 +308,10 @@ module Rack
|
|
309
308
|
expires: Time.at(0) }.merge(value))
|
310
309
|
|
311
310
|
end
|
312
|
-
module_function :add_remove_cookie_to_header
|
313
311
|
|
314
312
|
def rfc2822(time)
|
315
313
|
time.rfc2822
|
316
314
|
end
|
317
|
-
module_function :rfc2822
|
318
315
|
|
319
316
|
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
320
317
|
# of '% %b %Y'.
|
@@ -330,7 +327,6 @@ module Rack
|
|
330
327
|
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
|
331
328
|
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
332
329
|
end
|
333
|
-
module_function :rfc2109
|
334
330
|
|
335
331
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
336
332
|
# Returns nil if the header is missing or syntactically invalid.
|
@@ -339,7 +335,6 @@ module Rack
|
|
339
335
|
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
340
336
|
get_byte_ranges env['HTTP_RANGE'], size
|
341
337
|
end
|
342
|
-
module_function :byte_ranges
|
343
338
|
|
344
339
|
def get_byte_ranges(http_range, size)
|
345
340
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
@@ -368,7 +363,6 @@ module Rack
|
|
368
363
|
end
|
369
364
|
ranges
|
370
365
|
end
|
371
|
-
module_function :get_byte_ranges
|
372
366
|
|
373
367
|
# Constant time string comparison.
|
374
368
|
#
|
@@ -385,7 +379,6 @@ module Rack
|
|
385
379
|
b.each_byte { |v| r |= v ^ l[i += 1] }
|
386
380
|
r == 0
|
387
381
|
end
|
388
|
-
module_function :secure_compare
|
389
382
|
|
390
383
|
# Context allows the use of a compatible middleware at different points
|
391
384
|
# in a request handling stack. A compatible middleware must define
|
@@ -418,6 +411,14 @@ module Rack
|
|
418
411
|
#
|
419
412
|
# @api private
|
420
413
|
class HeaderHash < Hash # :nodoc:
|
414
|
+
def self.[](headers)
|
415
|
+
if headers.is_a?(HeaderHash) && !headers.frozen?
|
416
|
+
return headers
|
417
|
+
else
|
418
|
+
return self.new(headers)
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
421
422
|
def initialize(hash = {})
|
422
423
|
super()
|
423
424
|
@names = {}
|
@@ -430,6 +431,12 @@ module Rack
|
|
430
431
|
@names = other.names.dup
|
431
432
|
end
|
432
433
|
|
434
|
+
# on clear, we need to clear @names hash
|
435
|
+
def clear
|
436
|
+
super
|
437
|
+
@names.clear
|
438
|
+
end
|
439
|
+
|
433
440
|
def each
|
434
441
|
super do |k, v|
|
435
442
|
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
@@ -574,7 +581,6 @@ module Rack
|
|
574
581
|
status.to_i
|
575
582
|
end
|
576
583
|
end
|
577
|
-
module_function :status_code
|
578
584
|
|
579
585
|
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
580
586
|
|
@@ -588,18 +594,16 @@ module Rack
|
|
588
594
|
part == '..' ? clean.pop : clean << part
|
589
595
|
end
|
590
596
|
|
591
|
-
|
592
|
-
|
593
|
-
|
597
|
+
clean_path = clean.join(::File::SEPARATOR)
|
598
|
+
clean_path.prepend("/") if parts.empty? || parts.first.empty?
|
599
|
+
clean_path
|
594
600
|
end
|
595
|
-
module_function :clean_path_info
|
596
601
|
|
597
602
|
NULL_BYTE = "\0"
|
598
603
|
|
599
604
|
def valid_path?(path)
|
600
605
|
path.valid_encoding? && !path.include?(NULL_BYTE)
|
601
606
|
end
|
602
|
-
module_function :valid_path?
|
603
607
|
|
604
608
|
end
|
605
609
|
end
|
data/lib/rack/version.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
|
4
|
+
#
|
5
|
+
# Rack is freely distributable under the terms of an MIT-style license.
|
6
|
+
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
|
7
|
+
|
8
|
+
# The Rack main module, serving as a namespace for all core Rack
|
9
|
+
# modules and classes.
|
10
|
+
#
|
11
|
+
# All modules meant for use in your application are <tt>autoload</tt>ed here,
|
12
|
+
# so it should be enough just to <tt>require 'rack'</tt> in your code.
|
13
|
+
|
14
|
+
module Rack
|
15
|
+
# The Rack protocol version number implemented.
|
16
|
+
VERSION = [1, 3]
|
17
|
+
|
18
|
+
# Return the Rack protocol version as a dotted string.
|
19
|
+
def self.version
|
20
|
+
VERSION.join(".")
|
21
|
+
end
|
22
|
+
|
23
|
+
RELEASE = "2.2.2"
|
24
|
+
|
25
|
+
# Return the Rack release as a dotted string.
|
26
|
+
def self.release
|
27
|
+
RELEASE
|
28
|
+
end
|
29
|
+
end
|