forked from rack/rack
/
deflater.rb
137 lines (117 loc) · 3.8 KB
/
deflater.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
require "zlib"
require "stringio"
require "time" # for Time.httpdate
require 'rack/utils'
module Rack
class Deflater
def initialize(app, options = {})
@app = app
@min_content_length = options[:min_content_length] || options['min_content_length']
@skip_if = options[:skip_if] || options['skip_if']
@exclude = options[:exclude] || options['exclude']
end
def call(env)
status, headers, body = @app.call(env)
headers = Utils::HeaderHash.new(headers)
unless should_deflate?(env, status, headers, body)
return [status, headers, body]
end
request = Request.new(env)
encoding = Utils.select_best_encoding(%w(gzip deflate identity),
request.accept_encoding)
# Set the Vary HTTP header.
vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
unless vary.include?("*") || vary.include?("Accept-Encoding")
headers["Vary"] = vary.push("Accept-Encoding").join(",")
end
case encoding
when "gzip"
headers['Content-Encoding'] = "gzip"
headers.delete('Content-Length')
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
[status, headers, GzipStream.new(body, mtime)]
when "deflate"
headers['Content-Encoding'] = "deflate"
headers.delete('Content-Length')
[status, headers, DeflateStream.new(body)]
when "identity"
[status, headers, body]
when nil
body.close if body.respond_to?(:close)
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
[406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
end
end
class GzipStream
def initialize(body, mtime)
@body = body
@mtime = mtime
end
def each(&block)
@writer = block
gzip =::Zlib::GzipWriter.new(self)
gzip.mtime = @mtime
@body.each { |part|
gzip.write(part)
gzip.flush
}
ensure
@body.close if @body.respond_to?(:close)
gzip.close
@writer = nil
end
def write(data)
@writer.call(data)
end
end
class DeflateStream
DEFLATE_ARGS = [
Zlib::DEFAULT_COMPRESSION,
# drop the zlib header which causes both Safari and IE to choke
-Zlib::MAX_WBITS,
Zlib::DEF_MEM_LEVEL,
Zlib::DEFAULT_STRATEGY
]
def initialize(body)
@body = body
end
def each
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
@body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
yield deflater.finish
nil
ensure
@body.close if @body.respond_to?(:close)
deflater.close
end
end
private
def should_deflate?(env, status, headers, body)
# Skip compressing empty entity body responses and responses with
# no-transform set.
if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
return false
end
# Skip if response body is too short
if @min_content_length &&
@min_content_length > headers['Content-Length'].to_i
return false
end
# Skip if :exclude is provided and evaluates to true
if @exclude &&
@exclude.kind_of?(Regexp) &&
@exclude.match(env['PATH_INFO'])
return false
end
# Skip if :skip_if lambda is provided and evaluates to true
if @skip_if &&
@skip_if.call(env, status, headers, body)
return false
end
true
end
end
end