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.
- checksums.yaml +7 -0
- data/COPYING +1 -1
- data/HISTORY.md +375 -0
- data/KNOWN-ISSUES +23 -0
- data/README.rdoc +312 -0
- data/Rakefile +124 -0
- data/SPEC +125 -32
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +150 -0
- data/contrib/rack_logo.svg +1 -1
- data/contrib/rdoc.css +412 -0
- data/example/protectedlobster.rb +1 -1
- data/lib/rack/auth/abstract/handler.rb +4 -4
- data/lib/rack/auth/abstract/request.rb +7 -5
- data/lib/rack/auth/basic.rb +1 -1
- data/lib/rack/auth/digest/md5.rb +7 -3
- data/lib/rack/auth/digest/nonce.rb +1 -1
- data/lib/rack/auth/digest/params.rb +7 -9
- data/lib/rack/auth/digest/request.rb +10 -9
- data/lib/rack/backports/uri/common_18.rb +56 -0
- data/lib/rack/backports/uri/common_192.rb +52 -0
- data/lib/rack/backports/uri/common_193.rb +29 -0
- data/lib/rack/body_proxy.rb +39 -0
- data/lib/rack/builder.rb +106 -22
- data/lib/rack/cascade.rb +17 -6
- data/lib/rack/chunked.rb +44 -24
- data/lib/rack/commonlogger.rb +36 -13
- data/lib/rack/conditionalget.rb +49 -17
- data/lib/rack/config.rb +5 -0
- data/lib/rack/content_length.rb +14 -6
- data/lib/rack/content_type.rb +7 -1
- data/lib/rack/deflater.rb +73 -15
- data/lib/rack/directory.rb +18 -8
- data/lib/rack/etag.rb +59 -9
- data/lib/rack/file.rb +106 -44
- data/lib/rack/handler/cgi.rb +11 -11
- data/lib/rack/handler/fastcgi.rb +18 -6
- data/lib/rack/handler/lsws.rb +2 -4
- data/lib/rack/handler/mongrel.rb +22 -6
- data/lib/rack/handler/scgi.rb +16 -8
- data/lib/rack/handler/thin.rb +19 -4
- data/lib/rack/handler/webrick.rb +72 -19
- data/lib/rack/handler.rb +47 -14
- data/lib/rack/head.rb +10 -2
- data/lib/rack/lint.rb +260 -75
- data/lib/rack/lobster.rb +13 -8
- data/lib/rack/lock.rb +13 -3
- data/lib/rack/logger.rb +0 -2
- data/lib/rack/methodoverride.rb +27 -8
- data/lib/rack/mime.rb +625 -167
- data/lib/rack/mock.rb +78 -53
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +253 -0
- data/lib/rack/multipart/uploaded_file.rb +34 -0
- data/lib/rack/multipart.rb +34 -0
- data/lib/rack/nulllogger.rb +21 -2
- data/lib/rack/recursive.rb +10 -5
- data/lib/rack/reloader.rb +3 -2
- data/lib/rack/request.rb +201 -74
- data/lib/rack/response.rb +41 -28
- data/lib/rack/rewindable_input.rb +15 -11
- data/lib/rack/runtime.rb +16 -3
- data/lib/rack/sendfile.rb +47 -29
- data/lib/rack/server.rb +223 -47
- data/lib/rack/session/abstract/id.rb +289 -30
- data/lib/rack/session/cookie.rb +133 -44
- data/lib/rack/session/memcache.rb +30 -56
- data/lib/rack/session/pool.rb +19 -43
- data/lib/rack/showexceptions.rb +53 -15
- data/lib/rack/showstatus.rb +14 -7
- data/lib/rack/static.rb +124 -12
- data/lib/rack/tempfile_reaper.rb +22 -0
- data/lib/rack/urlmap.rb +49 -15
- data/lib/rack/utils/okjson.rb +600 -0
- data/lib/rack/utils.rb +363 -361
- data/lib/rack.rb +17 -23
- data/rack.gemspec +11 -20
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +5 -0
- data/test/builder/line.ru +1 -0
- data/test/builder/options.ru +2 -0
- data/test/cgi/assets/folder/test.js +1 -0
- data/test/cgi/assets/fonts/font.eot +1 -0
- data/test/cgi/assets/images/image.png +1 -0
- data/test/cgi/assets/index.html +1 -0
- data/test/cgi/assets/javascripts/app.js +1 -0
- data/test/cgi/assets/stylesheets/app.css +1 -0
- data/test/cgi/lighttpd.conf +26 -0
- data/test/cgi/rackup_stub.rb +6 -0
- data/test/cgi/sample_rackup.ru +5 -0
- data/test/cgi/test +9 -0
- data/test/cgi/test+directory/test+file +1 -0
- data/test/cgi/test.fcgi +8 -0
- data/test/cgi/test.ru +5 -0
- data/test/gemloader.rb +10 -0
- data/test/multipart/bad_robots +259 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +6 -0
- data/test/multipart/empty +10 -0
- data/test/multipart/fail_16384_nofile +814 -0
- data/test/multipart/file1.txt +1 -0
- data/test/multipart/filename_and_modification_param +7 -0
- data/test/multipart/filename_and_no_name +6 -0
- data/test/multipart/filename_with_escaped_quotes +6 -0
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
- data/test/multipart/filename_with_null_byte +7 -0
- data/test/multipart/filename_with_percent_escaped_quotes +6 -0
- data/test/multipart/filename_with_unescaped_percentages +6 -0
- data/test/multipart/filename_with_unescaped_percentages2 +6 -0
- data/test/multipart/filename_with_unescaped_percentages3 +6 -0
- data/test/multipart/filename_with_unescaped_quotes +6 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/invalid_character +6 -0
- data/test/multipart/mixed_files +21 -0
- data/test/multipart/nested +10 -0
- data/test/multipart/none +9 -0
- data/test/multipart/semicolon +6 -0
- data/test/multipart/text +15 -0
- data/test/multipart/three_files_three_fields +31 -0
- data/test/multipart/webkit +32 -0
- data/test/rackup/config.ru +31 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/{spec_rack_auth_basic.rb → spec_auth_basic.rb} +23 -15
- data/test/{spec_rack_auth_digest.rb → spec_auth_digest.rb} +56 -29
- data/test/spec_body_proxy.rb +85 -0
- data/test/spec_builder.rb +223 -0
- data/test/{spec_rack_cascade.rb → spec_cascade.rb} +28 -15
- data/test/{spec_rack_cgi.rb → spec_cgi.rb} +44 -31
- data/test/spec_chunked.rb +101 -0
- data/test/spec_commonlogger.rb +93 -0
- data/test/spec_conditionalget.rb +102 -0
- data/test/{spec_rack_config.rb → spec_config.rb} +6 -8
- data/test/spec_content_length.rb +85 -0
- data/test/spec_content_type.rb +45 -0
- data/test/spec_deflater.rb +339 -0
- data/test/{spec_rack_directory.rb → spec_directory.rb} +37 -10
- data/test/spec_etag.rb +107 -0
- data/test/{spec_rack_fastcgi.rb → spec_fastcgi.rb} +47 -29
- data/test/spec_file.rb +221 -0
- data/test/spec_handler.rb +72 -0
- data/test/spec_head.rb +45 -0
- data/test/{spec_rack_lint.rb → spec_lint.rb} +82 -60
- data/test/spec_lobster.rb +58 -0
- data/test/spec_lock.rb +164 -0
- data/test/spec_logger.rb +23 -0
- data/test/spec_methodoverride.rb +95 -0
- data/test/spec_mime.rb +51 -0
- data/test/{spec_rack_mock.rb → spec_mock.rb} +92 -38
- data/test/{spec_rack_mongrel.rb → spec_mongrel.rb} +46 -53
- data/test/spec_multipart.rb +600 -0
- data/test/spec_nulllogger.rb +20 -0
- data/test/spec_recursive.rb +72 -0
- data/test/spec_request.rb +1227 -0
- data/test/spec_response.rb +407 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_runtime.rb +49 -0
- data/test/spec_sendfile.rb +130 -0
- data/test/spec_server.rb +167 -0
- data/test/spec_session_abstract_id.rb +53 -0
- data/test/spec_session_cookie.rb +410 -0
- data/test/{spec_rack_session_memcache.rb → spec_session_memcache.rb} +119 -71
- data/test/{spec_rack_session_pool.rb → spec_session_pool.rb} +106 -69
- data/test/spec_showexceptions.rb +85 -0
- data/test/spec_showstatus.rb +103 -0
- data/test/spec_static.rb +145 -0
- data/test/spec_tempfile_reaper.rb +63 -0
- data/test/{spec_rack_thin.rb → spec_thin.rb} +35 -35
- data/test/{spec_rack_urlmap.rb → spec_urlmap.rb} +40 -19
- data/test/spec_utils.rb +647 -0
- data/test/spec_version.rb +17 -0
- data/test/spec_webrick.rb +184 -0
- data/test/static/another/index.html +1 -0
- data/test/static/index.html +1 -0
- data/test/testrequest.rb +78 -0
- data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
- metadata +220 -239
- data/RDOX +0 -0
- data/README +0 -592
- data/lib/rack/adapter/camping.rb +0 -22
- data/test/spec_auth.rb +0 -57
- data/test/spec_rack_builder.rb +0 -84
- data/test/spec_rack_camping.rb +0 -55
- data/test/spec_rack_chunked.rb +0 -62
- data/test/spec_rack_commonlogger.rb +0 -61
- data/test/spec_rack_conditionalget.rb +0 -41
- data/test/spec_rack_content_length.rb +0 -43
- data/test/spec_rack_content_type.rb +0 -30
- data/test/spec_rack_deflater.rb +0 -127
- data/test/spec_rack_etag.rb +0 -17
- data/test/spec_rack_file.rb +0 -75
- data/test/spec_rack_handler.rb +0 -43
- data/test/spec_rack_head.rb +0 -30
- data/test/spec_rack_lobster.rb +0 -45
- data/test/spec_rack_lock.rb +0 -38
- data/test/spec_rack_logger.rb +0 -21
- data/test/spec_rack_methodoverride.rb +0 -60
- data/test/spec_rack_nulllogger.rb +0 -13
- data/test/spec_rack_recursive.rb +0 -77
- data/test/spec_rack_request.rb +0 -594
- data/test/spec_rack_response.rb +0 -221
- data/test/spec_rack_rewindable_input.rb +0 -118
- data/test/spec_rack_runtime.rb +0 -35
- data/test/spec_rack_sendfile.rb +0 -86
- data/test/spec_rack_session_cookie.rb +0 -92
- data/test/spec_rack_showexceptions.rb +0 -21
- data/test/spec_rack_showstatus.rb +0 -72
- data/test/spec_rack_static.rb +0 -37
- data/test/spec_rack_utils.rb +0 -557
- data/test/spec_rack_webrick.rb +0 -130
- data/test/spec_rackup.rb +0 -164
data/lib/rack/conditionalget.rb
CHANGED
|
@@ -13,35 +13,67 @@ module Rack
|
|
|
13
13
|
# a conditional GET matches.
|
|
14
14
|
#
|
|
15
15
|
# Adapted from Michael Klishin's Merb implementation:
|
|
16
|
-
#
|
|
16
|
+
# https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
|
|
17
17
|
class ConditionalGet
|
|
18
18
|
def initialize(app)
|
|
19
19
|
@app = app
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def call(env)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
case env[REQUEST_METHOD]
|
|
24
|
+
when "GET", "HEAD"
|
|
25
|
+
status, headers, body = @app.call(env)
|
|
26
|
+
headers = Utils::HeaderHash.new(headers)
|
|
27
|
+
if status == 200 && fresh?(env, headers)
|
|
28
|
+
status = 304
|
|
29
|
+
headers.delete(CONTENT_TYPE)
|
|
30
|
+
headers.delete(CONTENT_LENGTH)
|
|
31
|
+
original_body = body
|
|
32
|
+
body = Rack::BodyProxy.new([]) do
|
|
33
|
+
original_body.close if original_body.respond_to?(:close)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
[status, headers, body]
|
|
37
|
+
else
|
|
38
|
+
@app.call(env)
|
|
32
39
|
end
|
|
33
|
-
[status, headers, body]
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
private
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
|
|
44
|
+
def fresh?(env, headers)
|
|
45
|
+
modified_since = env['HTTP_IF_MODIFIED_SINCE']
|
|
46
|
+
none_match = env['HTTP_IF_NONE_MATCH']
|
|
47
|
+
|
|
48
|
+
return false unless modified_since || none_match
|
|
49
|
+
|
|
50
|
+
success = true
|
|
51
|
+
success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
|
|
52
|
+
success &&= etag_matches?(none_match, headers) if none_match
|
|
53
|
+
success
|
|
39
54
|
end
|
|
40
55
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
last_modified == env['HTTP_IF_MODIFIED_SINCE']
|
|
56
|
+
def etag_matches?(none_match, headers)
|
|
57
|
+
etag = headers['ETag'] and etag == none_match
|
|
44
58
|
end
|
|
45
|
-
end
|
|
46
59
|
|
|
60
|
+
def modified_since?(modified_since, headers)
|
|
61
|
+
last_modified = to_rfc2822(headers['Last-Modified']) and
|
|
62
|
+
modified_since and
|
|
63
|
+
modified_since >= last_modified
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_rfc2822(since)
|
|
67
|
+
# shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
|
|
68
|
+
# anything shorter is invalid, this avoids exceptions for common cases
|
|
69
|
+
# most common being the empty string
|
|
70
|
+
if since && since.length >= 16
|
|
71
|
+
# NOTE: there is no trivial way to write this in a non execption way
|
|
72
|
+
# _rfc2822 returns a hash but is not that usable
|
|
73
|
+
Time.rfc2822(since) rescue nil
|
|
74
|
+
else
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
47
79
|
end
|
data/lib/rack/config.rb
CHANGED
data/lib/rack/content_length.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require 'rack/utils'
|
|
2
|
+
require 'rack/body_proxy'
|
|
2
3
|
|
|
3
4
|
module Rack
|
|
5
|
+
|
|
4
6
|
# Sets the Content-Length header on responses with fixed-length bodies.
|
|
5
7
|
class ContentLength
|
|
6
8
|
include Rack::Utils
|
|
@@ -13,14 +15,20 @@ module Rack
|
|
|
13
15
|
status, headers, body = @app.call(env)
|
|
14
16
|
headers = HeaderHash.new(headers)
|
|
15
17
|
|
|
16
|
-
if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
|
|
17
|
-
!headers[
|
|
18
|
+
if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
|
|
19
|
+
!headers[CONTENT_LENGTH] &&
|
|
18
20
|
!headers['Transfer-Encoding'] &&
|
|
19
|
-
|
|
21
|
+
body.respond_to?(:to_ary)
|
|
22
|
+
|
|
23
|
+
obody = body
|
|
24
|
+
body, length = [], 0
|
|
25
|
+
obody.each { |part| body << part; length += bytesize(part) }
|
|
26
|
+
|
|
27
|
+
body = BodyProxy.new(body) do
|
|
28
|
+
obody.close if obody.respond_to?(:close)
|
|
29
|
+
end
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
length = body.to_ary.inject(0) { |len, part| len + bytesize(part) }
|
|
23
|
-
headers['Content-Length'] = length.to_s
|
|
31
|
+
headers[CONTENT_LENGTH] = length.to_s
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
[status, headers, body]
|
data/lib/rack/content_type.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Rack
|
|
|
9
9
|
#
|
|
10
10
|
# When no content type argument is provided, "text/html" is assumed.
|
|
11
11
|
class ContentType
|
|
12
|
+
include Rack::Utils
|
|
13
|
+
|
|
12
14
|
def initialize(app, content_type = "text/html")
|
|
13
15
|
@app, @content_type = app, content_type
|
|
14
16
|
end
|
|
@@ -16,7 +18,11 @@ module Rack
|
|
|
16
18
|
def call(env)
|
|
17
19
|
status, headers, body = @app.call(env)
|
|
18
20
|
headers = Utils::HeaderHash.new(headers)
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
|
23
|
+
headers[CONTENT_TYPE] ||= @content_type
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
[status, headers, body]
|
|
21
27
|
end
|
|
22
28
|
end
|
data/lib/rack/deflater.rb
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
require "zlib"
|
|
2
|
-
require "stringio"
|
|
3
2
|
require "time" # for Time.httpdate
|
|
4
3
|
require 'rack/utils'
|
|
5
4
|
|
|
6
5
|
module Rack
|
|
6
|
+
# This middleware enables compression of http responses.
|
|
7
|
+
#
|
|
8
|
+
# Currently supported compression algorithms:
|
|
9
|
+
#
|
|
10
|
+
# * gzip
|
|
11
|
+
# * deflate
|
|
12
|
+
# * identity (no transformation)
|
|
13
|
+
#
|
|
14
|
+
# The middleware automatically detects when compression is supported
|
|
15
|
+
# and allowed. For example no transformation is made when a cache
|
|
16
|
+
# directive of 'no-transform' is present, or when the response status
|
|
17
|
+
# code is one that doesn't allow an entity body.
|
|
7
18
|
class Deflater
|
|
8
|
-
|
|
19
|
+
##
|
|
20
|
+
# Creates Rack::Deflater middleware.
|
|
21
|
+
#
|
|
22
|
+
# [app] rack app instance
|
|
23
|
+
# [options] hash of deflater options, i.e.
|
|
24
|
+
# 'if' - a lambda enabling / disabling deflation based on returned boolean value
|
|
25
|
+
# e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 }
|
|
26
|
+
# 'include' - a list of content types that should be compressed
|
|
27
|
+
def initialize(app, options = {})
|
|
9
28
|
@app = app
|
|
29
|
+
|
|
30
|
+
@condition = options[:if]
|
|
31
|
+
@compressible_types = options[:include]
|
|
10
32
|
end
|
|
11
33
|
|
|
12
34
|
def call(env)
|
|
13
35
|
status, headers, body = @app.call(env)
|
|
14
36
|
headers = Utils::HeaderHash.new(headers)
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
# no-transform set.
|
|
18
|
-
if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
|
|
19
|
-
headers['Cache-Control'].to_s =~ /\bno-transform\b/
|
|
38
|
+
unless should_deflate?(env, status, headers, body)
|
|
20
39
|
return [status, headers, body]
|
|
21
40
|
end
|
|
22
41
|
|
|
@@ -34,19 +53,20 @@ module Rack
|
|
|
34
53
|
case encoding
|
|
35
54
|
when "gzip"
|
|
36
55
|
headers['Content-Encoding'] = "gzip"
|
|
37
|
-
headers.delete(
|
|
56
|
+
headers.delete(CONTENT_LENGTH)
|
|
38
57
|
mtime = headers.key?("Last-Modified") ?
|
|
39
58
|
Time.httpdate(headers["Last-Modified"]) : Time.now
|
|
40
59
|
[status, headers, GzipStream.new(body, mtime)]
|
|
41
60
|
when "deflate"
|
|
42
61
|
headers['Content-Encoding'] = "deflate"
|
|
43
|
-
headers.delete(
|
|
62
|
+
headers.delete(CONTENT_LENGTH)
|
|
44
63
|
[status, headers, DeflateStream.new(body)]
|
|
45
64
|
when "identity"
|
|
46
65
|
[status, headers, body]
|
|
47
66
|
when nil
|
|
48
67
|
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
|
|
49
|
-
[
|
|
68
|
+
bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
|
|
69
|
+
[406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
|
|
50
70
|
end
|
|
51
71
|
end
|
|
52
72
|
|
|
@@ -54,14 +74,18 @@ module Rack
|
|
|
54
74
|
def initialize(body, mtime)
|
|
55
75
|
@body = body
|
|
56
76
|
@mtime = mtime
|
|
77
|
+
@closed = false
|
|
57
78
|
end
|
|
58
79
|
|
|
59
80
|
def each(&block)
|
|
60
81
|
@writer = block
|
|
61
82
|
gzip =::Zlib::GzipWriter.new(self)
|
|
62
83
|
gzip.mtime = @mtime
|
|
63
|
-
@body.each { |part|
|
|
64
|
-
|
|
84
|
+
@body.each { |part|
|
|
85
|
+
gzip.write(part)
|
|
86
|
+
gzip.flush
|
|
87
|
+
}
|
|
88
|
+
ensure
|
|
65
89
|
gzip.close
|
|
66
90
|
@writer = nil
|
|
67
91
|
end
|
|
@@ -69,6 +93,12 @@ module Rack
|
|
|
69
93
|
def write(data)
|
|
70
94
|
@writer.call(data)
|
|
71
95
|
end
|
|
96
|
+
|
|
97
|
+
def close
|
|
98
|
+
return if @closed
|
|
99
|
+
@closed = true
|
|
100
|
+
@body.close if @body.respond_to?(:close)
|
|
101
|
+
end
|
|
72
102
|
end
|
|
73
103
|
|
|
74
104
|
class DeflateStream
|
|
@@ -82,15 +112,43 @@ module Rack
|
|
|
82
112
|
|
|
83
113
|
def initialize(body)
|
|
84
114
|
@body = body
|
|
115
|
+
@closed = false
|
|
85
116
|
end
|
|
86
117
|
|
|
87
118
|
def each
|
|
88
|
-
|
|
89
|
-
@body.each { |part| yield
|
|
90
|
-
|
|
91
|
-
yield deflater.finish
|
|
119
|
+
deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS)
|
|
120
|
+
@body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) }
|
|
121
|
+
yield deflator.finish
|
|
92
122
|
nil
|
|
123
|
+
ensure
|
|
124
|
+
deflator.close
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def close
|
|
128
|
+
return if @closed
|
|
129
|
+
@closed = true
|
|
130
|
+
@body.close if @body.respond_to?(:close)
|
|
93
131
|
end
|
|
94
132
|
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def should_deflate?(env, status, headers, body)
|
|
137
|
+
# Skip compressing empty entity body responses and responses with
|
|
138
|
+
# no-transform set.
|
|
139
|
+
if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
|
|
140
|
+
headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
|
|
141
|
+
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
|
|
142
|
+
return false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Skip if @compressible_types are given and does not include request's content type
|
|
146
|
+
return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
|
|
147
|
+
|
|
148
|
+
# Skip if @condition lambda is given and evaluates to false
|
|
149
|
+
return false if @condition && !@condition.call(env, status, headers, body)
|
|
150
|
+
|
|
151
|
+
true
|
|
152
|
+
end
|
|
95
153
|
end
|
|
96
154
|
end
|
data/lib/rack/directory.rb
CHANGED
|
@@ -55,8 +55,8 @@ table { width:100%%; }
|
|
|
55
55
|
|
|
56
56
|
def _call(env)
|
|
57
57
|
@env = env
|
|
58
|
-
@script_name = env[
|
|
59
|
-
@path_info = Utils.unescape(env[
|
|
58
|
+
@script_name = env[SCRIPT_NAME]
|
|
59
|
+
@path_info = Utils.unescape(env[PATH_INFO])
|
|
60
60
|
|
|
61
61
|
if forbidden = check_forbidden
|
|
62
62
|
forbidden
|
|
@@ -72,7 +72,7 @@ table { width:100%%; }
|
|
|
72
72
|
body = "Forbidden\n"
|
|
73
73
|
size = Rack::Utils.bytesize(body)
|
|
74
74
|
return [403, {"Content-Type" => "text/plain",
|
|
75
|
-
|
|
75
|
+
CONTENT_LENGTH => size.to_s,
|
|
76
76
|
"X-Cascade" => "pass"}, [body]]
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -80,13 +80,17 @@ table { width:100%%; }
|
|
|
80
80
|
@files = [['../','Parent Directory','','','']]
|
|
81
81
|
glob = F.join(@path, '*')
|
|
82
82
|
|
|
83
|
+
url_head = (@script_name.split('/') + @path_info.split('/')).map do |part|
|
|
84
|
+
Rack::Utils.escape part
|
|
85
|
+
end
|
|
86
|
+
|
|
83
87
|
Dir[glob].sort.each do |node|
|
|
84
88
|
stat = stat(node)
|
|
85
89
|
next unless stat
|
|
86
90
|
basename = F.basename(node)
|
|
87
91
|
ext = F.extname(node)
|
|
88
92
|
|
|
89
|
-
url = F.join(
|
|
93
|
+
url = F.join(*url_head + [Rack::Utils.escape(basename)])
|
|
90
94
|
size = stat.size
|
|
91
95
|
type = stat.directory? ? 'directory' : Mime.mime_type(ext)
|
|
92
96
|
size = stat.directory? ? '-' : filesize_format(size)
|
|
@@ -97,7 +101,7 @@ table { width:100%%; }
|
|
|
97
101
|
@files << [ url, basename, size, type, mtime ]
|
|
98
102
|
end
|
|
99
103
|
|
|
100
|
-
return [ 200, {
|
|
104
|
+
return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
def stat(node, max = 10)
|
|
@@ -126,13 +130,13 @@ table { width:100%%; }
|
|
|
126
130
|
body = "Entity not found: #{@path_info}\n"
|
|
127
131
|
size = Rack::Utils.bytesize(body)
|
|
128
132
|
return [404, {"Content-Type" => "text/plain",
|
|
129
|
-
|
|
133
|
+
CONTENT_LENGTH => size.to_s,
|
|
130
134
|
"X-Cascade" => "pass"}, [body]]
|
|
131
135
|
end
|
|
132
136
|
|
|
133
137
|
def each
|
|
134
|
-
show_path = @path.sub(/^#{@root}/,'')
|
|
135
|
-
files = @files.map{|f| DIR_FILE % f }*"\n"
|
|
138
|
+
show_path = Rack::Utils.escape_html(@path.sub(/^#{@root}/,''))
|
|
139
|
+
files = @files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n"
|
|
136
140
|
page = DIR_PAGE % [ show_path, show_path , files ]
|
|
137
141
|
page.each_line{|l| yield l }
|
|
138
142
|
end
|
|
@@ -153,5 +157,11 @@ table { width:100%%; }
|
|
|
153
157
|
|
|
154
158
|
int.to_s + 'B'
|
|
155
159
|
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
# Assumes url is already escaped.
|
|
163
|
+
def DIR_FILE_escape url, *html
|
|
164
|
+
[url, *html.map { |e| Utils.escape_html(e) }]
|
|
165
|
+
end
|
|
156
166
|
end
|
|
157
167
|
end
|
data/lib/rack/etag.rb
CHANGED
|
@@ -1,23 +1,73 @@
|
|
|
1
1
|
require 'digest/md5'
|
|
2
2
|
|
|
3
3
|
module Rack
|
|
4
|
-
# Automatically sets the ETag header on all String bodies
|
|
4
|
+
# Automatically sets the ETag header on all String bodies.
|
|
5
|
+
#
|
|
6
|
+
# The ETag header is skipped if ETag or Last-Modified headers are sent or if
|
|
7
|
+
# a sendfile body (body.responds_to :to_path) is given (since such cases
|
|
8
|
+
# should be handled by apache/nginx).
|
|
9
|
+
#
|
|
10
|
+
# On initialization, you can pass two parameters: a Cache-Control directive
|
|
11
|
+
# used when Etag is absent and a directive when it is present. The first
|
|
12
|
+
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
|
|
5
13
|
class ETag
|
|
6
|
-
|
|
14
|
+
ETAG_STRING = 'ETag'.freeze
|
|
15
|
+
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
|
|
16
|
+
|
|
17
|
+
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
|
|
7
18
|
@app = app
|
|
19
|
+
@cache_control = cache_control
|
|
20
|
+
@no_cache_control = no_cache_control
|
|
8
21
|
end
|
|
9
22
|
|
|
10
23
|
def call(env)
|
|
11
24
|
status, headers, body = @app.call(env)
|
|
12
25
|
|
|
13
|
-
if !
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[
|
|
26
|
+
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
|
|
27
|
+
original_body = body
|
|
28
|
+
digest, new_body = digest_body(body)
|
|
29
|
+
body = Rack::BodyProxy.new(new_body) do
|
|
30
|
+
original_body.close if original_body.respond_to?(:close)
|
|
31
|
+
end
|
|
32
|
+
headers[ETAG_STRING] = %(W/"#{digest}") if digest
|
|
20
33
|
end
|
|
34
|
+
|
|
35
|
+
unless headers[CACHE_CONTROL]
|
|
36
|
+
if digest
|
|
37
|
+
headers[CACHE_CONTROL] = @cache_control if @cache_control
|
|
38
|
+
else
|
|
39
|
+
headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[status, headers, body]
|
|
21
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def etag_status?(status)
|
|
49
|
+
status == 200 || status == 201
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def etag_body?(body)
|
|
53
|
+
!body.respond_to?(:to_path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def skip_caching?(headers)
|
|
57
|
+
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
|
|
58
|
+
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def digest_body(body)
|
|
62
|
+
parts = []
|
|
63
|
+
digest = nil
|
|
64
|
+
|
|
65
|
+
body.each do |part|
|
|
66
|
+
parts << part
|
|
67
|
+
(digest ||= Digest::MD5.new) << part unless part.empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
[digest && digest.hexdigest, parts]
|
|
71
|
+
end
|
|
22
72
|
end
|
|
23
73
|
end
|
data/lib/rack/file.rb
CHANGED
|
@@ -3,20 +3,28 @@ require 'rack/utils'
|
|
|
3
3
|
require 'rack/mime'
|
|
4
4
|
|
|
5
5
|
module Rack
|
|
6
|
-
# Rack::File serves files below the +root+ given, according to the
|
|
6
|
+
# Rack::File serves files below the +root+ directory given, according to the
|
|
7
7
|
# path info of the Rack request.
|
|
8
|
+
# e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
|
|
9
|
+
# as http://localhost:9292/passwd
|
|
8
10
|
#
|
|
9
11
|
# Handlers can detect if bodies are a Rack::File, and use mechanisms
|
|
10
12
|
# like sendfile on the +path+.
|
|
11
13
|
|
|
12
14
|
class File
|
|
15
|
+
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
|
|
16
|
+
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
|
|
17
|
+
|
|
13
18
|
attr_accessor :root
|
|
14
19
|
attr_accessor :path
|
|
20
|
+
attr_accessor :cache_control
|
|
15
21
|
|
|
16
22
|
alias :to_path :path
|
|
17
23
|
|
|
18
|
-
def initialize(root)
|
|
24
|
+
def initialize(root, headers={}, default_mime = 'text/plain')
|
|
19
25
|
@root = root
|
|
26
|
+
@headers = headers
|
|
27
|
+
@default_mime = default_mime
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
def call(env)
|
|
@@ -26,65 +34,119 @@ module Rack
|
|
|
26
34
|
F = ::File
|
|
27
35
|
|
|
28
36
|
def _call(env)
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
|
|
38
|
+
return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
|
|
39
|
+
end
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
path_info = Utils.unescape(env[PATH_INFO])
|
|
42
|
+
clean_path_info = Utils.clean_path_info(path_info)
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
raise Errno::EPERM
|
|
39
|
-
end
|
|
44
|
+
@path = F.join(@root, clean_path_info)
|
|
45
|
+
|
|
46
|
+
available = begin
|
|
47
|
+
F.file?(@path) && F.readable?(@path)
|
|
40
48
|
rescue SystemCallError
|
|
41
|
-
|
|
49
|
+
false
|
|
42
50
|
end
|
|
43
|
-
end
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[body]]
|
|
52
|
+
if available
|
|
53
|
+
serving(env)
|
|
54
|
+
else
|
|
55
|
+
fail(404, "File not found: #{path_info}")
|
|
56
|
+
end
|
|
51
57
|
end
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
def serving(env)
|
|
60
|
+
if env["REQUEST_METHOD"] == "OPTIONS"
|
|
61
|
+
return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
|
|
62
|
+
end
|
|
63
|
+
last_modified = F.mtime(@path).httpdate
|
|
64
|
+
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
|
|
65
|
+
|
|
66
|
+
headers = { "Last-Modified" => last_modified }
|
|
67
|
+
headers[CONTENT_TYPE] = mime_type if mime_type
|
|
68
|
+
|
|
69
|
+
# Set custom headers
|
|
70
|
+
@headers.each { |field, content| headers[field] = content } if @headers
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
|
|
73
|
+
|
|
74
|
+
size = filesize
|
|
75
|
+
|
|
76
|
+
ranges = Rack::Utils.byte_ranges(env, size)
|
|
77
|
+
if ranges.nil? || ranges.length > 1
|
|
78
|
+
# No ranges, or multiple ranges (which we don't support):
|
|
79
|
+
# TODO: Support multiple byte-ranges
|
|
80
|
+
response[0] = 200
|
|
81
|
+
@range = 0..size-1
|
|
82
|
+
elsif ranges.empty?
|
|
83
|
+
# Unsatisfiable. Return error, and file size:
|
|
84
|
+
response = fail(416, "Byte range unsatisfiable")
|
|
85
|
+
response[1]["Content-Range"] = "bytes */#{size}"
|
|
86
|
+
return response
|
|
62
87
|
else
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
# Partial content:
|
|
89
|
+
@range = ranges[0]
|
|
90
|
+
response[0] = 206
|
|
91
|
+
response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
|
|
92
|
+
size = @range.end - @range.begin + 1
|
|
65
93
|
end
|
|
66
94
|
|
|
67
|
-
[
|
|
68
|
-
"Last-Modified" => F.mtime(@path).httpdate,
|
|
69
|
-
"Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'),
|
|
70
|
-
"Content-Length" => size.to_s
|
|
71
|
-
}, body]
|
|
72
|
-
end
|
|
95
|
+
response[2] = [response_body] unless response_body.nil?
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
[404, {"Content-Type" => "text/plain",
|
|
77
|
-
"Content-Length" => body.size.to_s,
|
|
78
|
-
"X-Cascade" => "pass"},
|
|
79
|
-
[body]]
|
|
97
|
+
response[1][CONTENT_LENGTH] = size.to_s
|
|
98
|
+
response
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
def each
|
|
83
|
-
F.open(@path, "rb")
|
|
84
|
-
|
|
102
|
+
F.open(@path, "rb") do |file|
|
|
103
|
+
file.seek(@range.begin)
|
|
104
|
+
remaining_len = @range.end-@range.begin+1
|
|
105
|
+
while remaining_len > 0
|
|
106
|
+
part = file.read([8192, remaining_len].min)
|
|
107
|
+
break unless part
|
|
108
|
+
remaining_len -= part.length
|
|
109
|
+
|
|
85
110
|
yield part
|
|
86
111
|
end
|
|
87
|
-
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def fail(status, body, headers = {})
|
|
118
|
+
body += "\n"
|
|
119
|
+
[
|
|
120
|
+
status,
|
|
121
|
+
{
|
|
122
|
+
CONTENT_TYPE => "text/plain",
|
|
123
|
+
CONTENT_LENGTH => body.size.to_s,
|
|
124
|
+
"X-Cascade" => "pass"
|
|
125
|
+
}.merge!(headers),
|
|
126
|
+
[body]
|
|
127
|
+
]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The MIME type for the contents of the file located at @path
|
|
131
|
+
def mime_type
|
|
132
|
+
Mime.mime_type(F.extname(@path), @default_mime)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def filesize
|
|
136
|
+
# If response_body is present, use its size.
|
|
137
|
+
return Rack::Utils.bytesize(response_body) if response_body
|
|
138
|
+
|
|
139
|
+
# We check via File::size? whether this file provides size info
|
|
140
|
+
# via stat (e.g. /proc files often don't), otherwise we have to
|
|
141
|
+
# figure it out by reading the whole file into memory.
|
|
142
|
+
F.size?(@path) || Utils.bytesize(F.read(@path))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# By default, the response body for file requests is nil.
|
|
146
|
+
# In this case, the response body will be generated later
|
|
147
|
+
# from the file at @path
|
|
148
|
+
def response_body
|
|
149
|
+
nil
|
|
88
150
|
end
|
|
89
151
|
end
|
|
90
152
|
end
|