eac-rack 1.1.1
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.
- data/COPYING +18 -0
- data/KNOWN-ISSUES +21 -0
- data/README +399 -0
- data/bin/rackup +4 -0
- data/contrib/rack_logo.svg +111 -0
- data/example/lobster.ru +4 -0
- data/example/protectedlobster.rb +14 -0
- data/example/protectedlobster.ru +8 -0
- data/lib/rack.rb +92 -0
- data/lib/rack/adapter/camping.rb +22 -0
- data/lib/rack/auth/abstract/handler.rb +37 -0
- data/lib/rack/auth/abstract/request.rb +37 -0
- data/lib/rack/auth/basic.rb +58 -0
- data/lib/rack/auth/digest/md5.rb +124 -0
- data/lib/rack/auth/digest/nonce.rb +51 -0
- data/lib/rack/auth/digest/params.rb +55 -0
- data/lib/rack/auth/digest/request.rb +40 -0
- data/lib/rack/builder.rb +80 -0
- data/lib/rack/cascade.rb +41 -0
- data/lib/rack/chunked.rb +49 -0
- data/lib/rack/commonlogger.rb +49 -0
- data/lib/rack/conditionalget.rb +47 -0
- data/lib/rack/config.rb +15 -0
- data/lib/rack/content_length.rb +29 -0
- data/lib/rack/content_type.rb +23 -0
- data/lib/rack/deflater.rb +96 -0
- data/lib/rack/directory.rb +157 -0
- data/lib/rack/etag.rb +23 -0
- data/lib/rack/file.rb +90 -0
- data/lib/rack/handler.rb +88 -0
- data/lib/rack/handler/cgi.rb +61 -0
- data/lib/rack/handler/evented_mongrel.rb +8 -0
- data/lib/rack/handler/fastcgi.rb +89 -0
- data/lib/rack/handler/lsws.rb +63 -0
- data/lib/rack/handler/mongrel.rb +90 -0
- data/lib/rack/handler/scgi.rb +62 -0
- data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
- data/lib/rack/handler/thin.rb +18 -0
- data/lib/rack/handler/webrick.rb +69 -0
- data/lib/rack/head.rb +19 -0
- data/lib/rack/lint.rb +575 -0
- data/lib/rack/lobster.rb +65 -0
- data/lib/rack/lock.rb +16 -0
- data/lib/rack/logger.rb +20 -0
- data/lib/rack/methodoverride.rb +27 -0
- data/lib/rack/mime.rb +206 -0
- data/lib/rack/mock.rb +189 -0
- data/lib/rack/nulllogger.rb +18 -0
- data/lib/rack/recursive.rb +57 -0
- data/lib/rack/reloader.rb +109 -0
- data/lib/rack/request.rb +271 -0
- data/lib/rack/response.rb +149 -0
- data/lib/rack/rewindable_input.rb +100 -0
- data/lib/rack/runtime.rb +27 -0
- data/lib/rack/sendfile.rb +142 -0
- data/lib/rack/server.rb +212 -0
- data/lib/rack/session/abstract/id.rb +140 -0
- data/lib/rack/session/cookie.rb +90 -0
- data/lib/rack/session/memcache.rb +119 -0
- data/lib/rack/session/pool.rb +100 -0
- data/lib/rack/showexceptions.rb +349 -0
- data/lib/rack/showstatus.rb +106 -0
- data/lib/rack/static.rb +38 -0
- data/lib/rack/urlmap.rb +56 -0
- data/lib/rack/utils.rb +614 -0
- data/rack.gemspec +38 -0
- data/test/spec_rack_auth_basic.rb +73 -0
- data/test/spec_rack_auth_digest.rb +226 -0
- data/test/spec_rack_builder.rb +84 -0
- data/test/spec_rack_camping.rb +51 -0
- data/test/spec_rack_cascade.rb +48 -0
- data/test/spec_rack_cgi.rb +89 -0
- data/test/spec_rack_chunked.rb +62 -0
- data/test/spec_rack_commonlogger.rb +61 -0
- data/test/spec_rack_conditionalget.rb +41 -0
- data/test/spec_rack_config.rb +24 -0
- data/test/spec_rack_content_length.rb +43 -0
- data/test/spec_rack_content_type.rb +30 -0
- data/test/spec_rack_deflater.rb +127 -0
- data/test/spec_rack_directory.rb +61 -0
- data/test/spec_rack_etag.rb +17 -0
- data/test/spec_rack_fastcgi.rb +89 -0
- data/test/spec_rack_file.rb +75 -0
- data/test/spec_rack_handler.rb +43 -0
- data/test/spec_rack_head.rb +30 -0
- data/test/spec_rack_lint.rb +528 -0
- data/test/spec_rack_lobster.rb +45 -0
- data/test/spec_rack_lock.rb +38 -0
- data/test/spec_rack_logger.rb +21 -0
- data/test/spec_rack_methodoverride.rb +60 -0
- data/test/spec_rack_mock.rb +243 -0
- data/test/spec_rack_mongrel.rb +189 -0
- data/test/spec_rack_nulllogger.rb +13 -0
- data/test/spec_rack_recursive.rb +77 -0
- data/test/spec_rack_request.rb +545 -0
- data/test/spec_rack_response.rb +221 -0
- data/test/spec_rack_rewindable_input.rb +118 -0
- data/test/spec_rack_runtime.rb +35 -0
- data/test/spec_rack_sendfile.rb +86 -0
- data/test/spec_rack_session_cookie.rb +73 -0
- data/test/spec_rack_session_memcache.rb +273 -0
- data/test/spec_rack_session_pool.rb +172 -0
- data/test/spec_rack_showexceptions.rb +21 -0
- data/test/spec_rack_showstatus.rb +72 -0
- data/test/spec_rack_static.rb +37 -0
- data/test/spec_rack_thin.rb +91 -0
- data/test/spec_rack_urlmap.rb +215 -0
- data/test/spec_rack_utils.rb +554 -0
- data/test/spec_rack_webrick.rb +130 -0
- data/test/spec_rackup.rb +154 -0
- metadata +311 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rack
|
2
|
+
module Auth
|
3
|
+
module Digest
|
4
|
+
class Params < Hash
|
5
|
+
|
6
|
+
def self.parse(str)
|
7
|
+
split_header_value(str).inject(new) do |header, param|
|
8
|
+
k, v = param.split('=', 2)
|
9
|
+
header[k] = dequote(v)
|
10
|
+
header
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.dequote(str) # From WEBrick::HTTPUtils
|
15
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
16
|
+
ret.gsub!(/\\(.)/, "\\1")
|
17
|
+
ret
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.split_header_value(str)
|
21
|
+
str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
super
|
26
|
+
|
27
|
+
yield self if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](k)
|
31
|
+
super k.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def []=(k, v)
|
35
|
+
super k.to_s, v.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
UNQUOTED = ['qop', 'nc', 'stale']
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
inject([]) do |parts, (k, v)|
|
42
|
+
parts << "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v))
|
43
|
+
parts
|
44
|
+
end.join(', ')
|
45
|
+
end
|
46
|
+
|
47
|
+
def quote(str) # From WEBrick::HTTPUtils
|
48
|
+
'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rack/auth/abstract/request'
|
2
|
+
require 'rack/auth/digest/params'
|
3
|
+
require 'rack/auth/digest/nonce'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Auth
|
7
|
+
module Digest
|
8
|
+
class Request < Auth::AbstractRequest
|
9
|
+
|
10
|
+
def method
|
11
|
+
@env['rack.methodoverride.original_method'] || @env['REQUEST_METHOD']
|
12
|
+
end
|
13
|
+
|
14
|
+
def digest?
|
15
|
+
:digest == scheme
|
16
|
+
end
|
17
|
+
|
18
|
+
def correct_uri?
|
19
|
+
(@env['SCRIPT_NAME'].to_s + @env['PATH_INFO'].to_s) == uri
|
20
|
+
end
|
21
|
+
|
22
|
+
def nonce
|
23
|
+
@nonce ||= Nonce.parse(params['nonce'])
|
24
|
+
end
|
25
|
+
|
26
|
+
def params
|
27
|
+
@params ||= Params.parse(parts.last)
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(sym)
|
31
|
+
if params.has_key? key = sym.to_s
|
32
|
+
return params[key]
|
33
|
+
end
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/rack/builder.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::Builder implements a small DSL to iteratively construct Rack
|
3
|
+
# applications.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# app = Rack::Builder.new {
|
8
|
+
# use Rack::CommonLogger
|
9
|
+
# use Rack::ShowExceptions
|
10
|
+
# map "/lobster" do
|
11
|
+
# use Rack::Lint
|
12
|
+
# run Rack::Lobster.new
|
13
|
+
# end
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
# Or
|
17
|
+
#
|
18
|
+
# app = Rack::Builder.app do
|
19
|
+
# use Rack::CommonLogger
|
20
|
+
# lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# +use+ adds a middleware to the stack, +run+ dispatches to an application.
|
24
|
+
# You can use +map+ to construct a Rack::URLMap in a convenient way.
|
25
|
+
|
26
|
+
class Builder
|
27
|
+
def self.parse_file(config, opts = Server::Options.new)
|
28
|
+
options = {}
|
29
|
+
if config =~ /\.ru$/
|
30
|
+
cfgfile = ::File.read(config)
|
31
|
+
if cfgfile[/^#\\(.*)/] && opts
|
32
|
+
options = opts.parse! $1.split(/\s+/)
|
33
|
+
end
|
34
|
+
cfgfile.sub!(/^__END__\n.*/, '')
|
35
|
+
app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
|
36
|
+
TOPLEVEL_BINDING, config
|
37
|
+
else
|
38
|
+
require config
|
39
|
+
app = Object.const_get(::File.basename(config, '.rb').capitalize)
|
40
|
+
end
|
41
|
+
return app, options
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(&block)
|
45
|
+
@ins = []
|
46
|
+
instance_eval(&block) if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.app(&block)
|
50
|
+
self.new(&block).to_app
|
51
|
+
end
|
52
|
+
|
53
|
+
def use(middleware, *args, &block)
|
54
|
+
@ins << lambda { |app| middleware.new(app, *args, &block) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def run(app)
|
58
|
+
@ins << app #lambda { |nothing| app }
|
59
|
+
end
|
60
|
+
|
61
|
+
def map(path, &block)
|
62
|
+
if @ins.last.kind_of? Hash
|
63
|
+
@ins.last[path] = self.class.new(&block).to_app
|
64
|
+
else
|
65
|
+
@ins << {}
|
66
|
+
map(path, &block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_app
|
71
|
+
@ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last
|
72
|
+
inner_app = @ins.last
|
73
|
+
@ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(env)
|
77
|
+
to_app.call(env)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/rack/cascade.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::Cascade tries an request on several apps, and returns the
|
3
|
+
# first response that is not 404 (or in a list of configurable
|
4
|
+
# status codes).
|
5
|
+
|
6
|
+
class Cascade
|
7
|
+
NotFound = [404, {}, []]
|
8
|
+
|
9
|
+
attr_reader :apps
|
10
|
+
|
11
|
+
def initialize(apps, catch=404)
|
12
|
+
@apps = []; @has_app = {}
|
13
|
+
apps.each { |app| add app }
|
14
|
+
|
15
|
+
@catch = {}
|
16
|
+
[*catch].each { |status| @catch[status] = true }
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
result = NotFound
|
21
|
+
|
22
|
+
@apps.each do |app|
|
23
|
+
result = app.call(env)
|
24
|
+
break unless @catch.include?(result[0].to_i)
|
25
|
+
end
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def add app
|
31
|
+
@has_app[app] = true
|
32
|
+
@apps << app
|
33
|
+
end
|
34
|
+
|
35
|
+
def include? app
|
36
|
+
@has_app.include? app
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method :<<, :add
|
40
|
+
end
|
41
|
+
end
|
data/lib/rack/chunked.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
# Middleware that applies chunked transfer encoding to response bodies
|
6
|
+
# when the response does not include a Content-Length header.
|
7
|
+
class Chunked
|
8
|
+
include Rack::Utils
|
9
|
+
|
10
|
+
def initialize(app)
|
11
|
+
@app = app
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
status, headers, body = @app.call(env)
|
16
|
+
headers = HeaderHash.new(headers)
|
17
|
+
|
18
|
+
if env['HTTP_VERSION'] == 'HTTP/1.0' ||
|
19
|
+
STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
|
20
|
+
headers['Content-Length'] ||
|
21
|
+
headers['Transfer-Encoding']
|
22
|
+
[status, headers, body]
|
23
|
+
else
|
24
|
+
dup.chunk(status, headers, body)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def chunk(status, headers, body)
|
29
|
+
@body = body
|
30
|
+
headers.delete('Content-Length')
|
31
|
+
headers['Transfer-Encoding'] = 'chunked'
|
32
|
+
[status, headers, self]
|
33
|
+
end
|
34
|
+
|
35
|
+
def each
|
36
|
+
term = "\r\n"
|
37
|
+
@body.each do |chunk|
|
38
|
+
size = bytesize(chunk)
|
39
|
+
next if size == 0
|
40
|
+
yield [size.to_s(16), term, chunk, term].join
|
41
|
+
end
|
42
|
+
yield ["0", term, "", term].join
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
@body.close if @body.respond_to?(:close)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::CommonLogger forwards every request to an +app+ given, and
|
3
|
+
# logs a line in the Apache common log format to the +logger+, or
|
4
|
+
# rack.errors by default.
|
5
|
+
class CommonLogger
|
6
|
+
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
|
7
|
+
# lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
|
8
|
+
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
|
9
|
+
FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
|
10
|
+
|
11
|
+
def initialize(app, logger=nil)
|
12
|
+
@app = app
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
began_at = Time.now
|
18
|
+
status, header, body = @app.call(env)
|
19
|
+
header = Utils::HeaderHash.new(header)
|
20
|
+
log(env, status, header, began_at)
|
21
|
+
[status, header, body]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def log(env, status, header, began_at)
|
27
|
+
now = Time.now
|
28
|
+
length = extract_content_length(header)
|
29
|
+
|
30
|
+
logger = @logger || env['rack.errors']
|
31
|
+
logger.write FORMAT % [
|
32
|
+
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
|
33
|
+
env["REMOTE_USER"] || "-",
|
34
|
+
now.strftime("%d/%b/%Y %H:%M:%S"),
|
35
|
+
env["REQUEST_METHOD"],
|
36
|
+
env["PATH_INFO"],
|
37
|
+
env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
|
38
|
+
env["HTTP_VERSION"],
|
39
|
+
status.to_s[0..3],
|
40
|
+
length,
|
41
|
+
now - began_at ]
|
42
|
+
end
|
43
|
+
|
44
|
+
def extract_content_length(headers)
|
45
|
+
value = headers['Content-Length'] or return '-'
|
46
|
+
value.to_s == '0' ? '-' : value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
# Middleware that enables conditional GET using If-None-Match and
|
6
|
+
# If-Modified-Since. The application should set either or both of the
|
7
|
+
# Last-Modified or Etag response headers according to RFC 2616. When
|
8
|
+
# either of the conditions is met, the response body is set to be zero
|
9
|
+
# length and the response status is set to 304 Not Modified.
|
10
|
+
#
|
11
|
+
# Applications that defer response body generation until the body's each
|
12
|
+
# message is received will avoid response body generation completely when
|
13
|
+
# a conditional GET matches.
|
14
|
+
#
|
15
|
+
# Adapted from Michael Klishin's Merb implementation:
|
16
|
+
# http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb
|
17
|
+
class ConditionalGet
|
18
|
+
def initialize(app)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
return @app.call(env) unless %w[GET HEAD].include?(env['REQUEST_METHOD'])
|
24
|
+
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
headers = Utils::HeaderHash.new(headers)
|
27
|
+
if etag_matches?(env, headers) || modified_since?(env, headers)
|
28
|
+
status = 304
|
29
|
+
headers.delete('Content-Type')
|
30
|
+
headers.delete('Content-Length')
|
31
|
+
body = []
|
32
|
+
end
|
33
|
+
[status, headers, body]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def etag_matches?(env, headers)
|
38
|
+
etag = headers['Etag'] and etag == env['HTTP_IF_NONE_MATCH']
|
39
|
+
end
|
40
|
+
|
41
|
+
def modified_since?(env, headers)
|
42
|
+
last_modified = headers['Last-Modified'] and
|
43
|
+
last_modified == env['HTTP_IF_MODIFIED_SINCE']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/lib/rack/config.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::Config modifies the environment using the block given during
|
3
|
+
# initialization.
|
4
|
+
class Config
|
5
|
+
def initialize(app, &block)
|
6
|
+
@app = app
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
@block.call(env)
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Sets the Content-Length header on responses with fixed-length bodies.
|
5
|
+
class ContentLength
|
6
|
+
include Rack::Utils
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
status, headers, body = @app.call(env)
|
14
|
+
headers = HeaderHash.new(headers)
|
15
|
+
|
16
|
+
if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
|
17
|
+
!headers['Content-Length'] &&
|
18
|
+
!headers['Transfer-Encoding'] &&
|
19
|
+
(body.respond_to?(:to_ary) || body.respond_to?(:to_str))
|
20
|
+
|
21
|
+
body = [body] if body.respond_to?(:to_str) # rack 0.4 compat
|
22
|
+
length = body.to_ary.inject(0) { |len, part| len + bytesize(part) }
|
23
|
+
headers['Content-Length'] = length.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
[status, headers, body]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
# Sets the Content-Type header on responses which don't have one.
|
6
|
+
#
|
7
|
+
# Builder Usage:
|
8
|
+
# use Rack::ContentType, "text/plain"
|
9
|
+
#
|
10
|
+
# When no content type argument is provided, "text/html" is assumed.
|
11
|
+
class ContentType
|
12
|
+
def initialize(app, content_type = "text/html")
|
13
|
+
@app, @content_type = app, content_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
status, headers, body = @app.call(env)
|
18
|
+
headers = Utils::HeaderHash.new(headers)
|
19
|
+
headers['Content-Type'] ||= @content_type
|
20
|
+
[status, headers, body]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "zlib"
|
2
|
+
require "stringio"
|
3
|
+
require "time" # for Time.httpdate
|
4
|
+
require 'rack/utils'
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
class Deflater
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
status, headers, body = @app.call(env)
|
14
|
+
headers = Utils::HeaderHash.new(headers)
|
15
|
+
|
16
|
+
# Skip compressing empty entity body responses and responses with
|
17
|
+
# no-transform set.
|
18
|
+
if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
|
19
|
+
headers['Cache-Control'].to_s =~ /\bno-transform\b/
|
20
|
+
return [status, headers, body]
|
21
|
+
end
|
22
|
+
|
23
|
+
request = Request.new(env)
|
24
|
+
|
25
|
+
encoding = Utils.select_best_encoding(%w(gzip deflate identity),
|
26
|
+
request.accept_encoding)
|
27
|
+
|
28
|
+
# Set the Vary HTTP header.
|
29
|
+
vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
|
30
|
+
unless vary.include?("*") || vary.include?("Accept-Encoding")
|
31
|
+
headers["Vary"] = vary.push("Accept-Encoding").join(",")
|
32
|
+
end
|
33
|
+
|
34
|
+
case encoding
|
35
|
+
when "gzip"
|
36
|
+
headers['Content-Encoding'] = "gzip"
|
37
|
+
headers.delete('Content-Length')
|
38
|
+
mtime = headers.key?("Last-Modified") ?
|
39
|
+
Time.httpdate(headers["Last-Modified"]) : Time.now
|
40
|
+
[status, headers, GzipStream.new(body, mtime)]
|
41
|
+
when "deflate"
|
42
|
+
headers['Content-Encoding'] = "deflate"
|
43
|
+
headers.delete('Content-Length')
|
44
|
+
[status, headers, DeflateStream.new(body)]
|
45
|
+
when "identity"
|
46
|
+
[status, headers, body]
|
47
|
+
when nil
|
48
|
+
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
|
49
|
+
[406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class GzipStream
|
54
|
+
def initialize(body, mtime)
|
55
|
+
@body = body
|
56
|
+
@mtime = mtime
|
57
|
+
end
|
58
|
+
|
59
|
+
def each(&block)
|
60
|
+
@writer = block
|
61
|
+
gzip =::Zlib::GzipWriter.new(self)
|
62
|
+
gzip.mtime = @mtime
|
63
|
+
@body.each { |part| gzip.write(part) }
|
64
|
+
@body.close if @body.respond_to?(:close)
|
65
|
+
gzip.close
|
66
|
+
@writer = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(data)
|
70
|
+
@writer.call(data)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class DeflateStream
|
75
|
+
DEFLATE_ARGS = [
|
76
|
+
Zlib::DEFAULT_COMPRESSION,
|
77
|
+
# drop the zlib header which causes both Safari and IE to choke
|
78
|
+
-Zlib::MAX_WBITS,
|
79
|
+
Zlib::DEF_MEM_LEVEL,
|
80
|
+
Zlib::DEFAULT_STRATEGY
|
81
|
+
]
|
82
|
+
|
83
|
+
def initialize(body)
|
84
|
+
@body = body
|
85
|
+
end
|
86
|
+
|
87
|
+
def each
|
88
|
+
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
|
89
|
+
@body.each { |part| yield deflater.deflate(part) }
|
90
|
+
@body.close if @body.respond_to?(:close)
|
91
|
+
yield deflater.finish
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|