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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
| | # -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
# License: GPL-3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
# frozen_string_literal: true
require 'time'
require 'rack/utils'
require 'rack/mime'
require 'kgio'
class TryGzipStatic
attr_accessor :root
class KF < Kgio::File
# attr_writer :sf_range
# only used if the server does not handle #to_path,
# we actually hit this if serving the gzipped file in the first place,
# _and_ Rack::Deflater is used in the middleware stack. Oh well...
def each
buf = ''.dup
rsize = 8192
if @sf_range
file.seek(@sf_range.begin)
sf_count = @sf_range.end - @sf_range.begin + 1
while sf_count > 0
read(sf_count > rsize ? rsize : sf_count, buf) or break
sf_count -= buf.size
yield buf
end
raise "file truncated" if sf_count != 0
else
yield(buf) while read(rsize, buf)
end
end
end
def initialize(root, default_type = 'text/plain')
@root = root.b
@default_type = default_type
end
def fspath(env)
path_info = Rack::Utils.unescape(env["PATH_INFO"], Encoding::BINARY)
path_info =~ /\.\./ ? nil : "#@root#{path_info}"
end
def get_range(env, path, st)
if ims = env["HTTP_IF_MODIFIED_SINCE"]
return [ 304, {}, [] ] if st.mtime.httpdate == ims
end
size = st.size
ranges = Rack::Utils.byte_ranges(env, size)
if ranges.nil? || ranges.length > 1
[ 200 ] # serve the whole thing, possibly with static gzip \o/
elsif ranges.empty?
res = r(416)
res[1]["Content-Range"] = "bytes */#{size}"
res
else # partial response, no using static gzip file
range = ranges[0]
len = range.end - range.begin + 1
h = fheader(env, path, st, nil, len)
h["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
[ 206, h, range ]
end
end
def fheader(env, path, st, gz_st = nil, len = nil)
if path =~ /(.[^.]+)\z/
mime = Rack::Mime.mime_type($1, @default_type)
else
mime = @default_type
end
len ||= (gz_st ? gz_st : st).size
h = {
"Content-Type" => mime,
"Content-Length" => len.to_s,
"Last-Modified" => st.mtime.httpdate,
"Accept-Ranges" => "bytes",
}
h["Cache-Control"] = "no-transform" unless mime =~ %r{\Atext\/}
h["Content-Encoding"] = "gzip" if gz_st
h
end
def head_no_gz(res, env, path, st)
res[1] = fheader(env, path, st)
res[2] = [] # empty body
res
end
def stat_path(env)
path = fspath(env) or return r(403)
begin
st = File.lstat(path)
if st.symlink?
path = File.readlink(path)
path[0] == '/'.freeze or path = "#@root/#{path}"
st = File.stat(path)
end
return r(404) unless st.file?
return r(403) unless st.readable?
[ path, st ]
rescue Errno::ENOENT, Errno::ENOTDIR
r(404)
rescue Errno::EACCES
r(403)
rescue => e
r(500, e, env)
end
end
def head(env)
path, st = res = stat_path(env)
return res if Integer === path # integer status code on failure
# see if it's a range request, no gzipped version if so
status, _ = res = get_range(env, path, st)
case status
when 206
res[2] = [] # empty body, headers are all set
res
when 200 # fall through to trying gzipped version
# client requested gzipped path explicitly or did not want gzip
if path =~ /\.gz\z/i || !want_gzip?(env)
head_no_gz(res, env, path, st)
else # try the gzipped version
begin
gz_st = File.stat("#{path}.gz")
if gz_st.mtime == st.mtime
res[1] = fheader(env, path, st, gz_st)
res[2] = []
res
else
head_no_gz(res, env, path, st)
end
rescue Errno::ENOENT, Errno::EACCES
head_no_gz(res, env, path, st)
rescue => e
r(500, e, env)
end
end
else # 416, 304
res
end
end
def call(env)
case env["REQUEST_METHOD"]
when "GET" then get(env)
when "HEAD" then head(env)
else r(405)
end
end
def want_gzip?(env)
env["HTTP_ACCEPT_ENCODING"] =~ /\bgzip\b/i
end
def get(env)
path, st, _ = res = stat_path(env)
return res if Integer === path # integer status code on failure
# see if it's a range request, no gzipped version if so
status, _, _ = res = get_range(env, path, st)
case status
when 206
res[2] = KF.open(path) # stat succeeded
when 200
# client requested gzipped path explicitly or did not want gzip
if path =~ /\.gz\z/i || !want_gzip?(env)
res[1] = fheader(env, path, st)
res[2] = KF.open(path)
else
case gzbody = KF.tryopen("#{path}.gz")
when KF
gz_st = gzbody.stat
if gz_st.file? && gz_st.mtime == st.mtime
# yay! serve the gzipped version as the regular one
# this should be the most likely code path
res[1] = fheader(env, path, st, gz_st)
res[2] = gzbody
else
gzbody.close
res[1] = fheader(env, path, st)
res[2] = KF.open(path)
end
when :ENOENT, :EACCES
res[1] = fheader(env, path, st)
res[2] = KF.open(path)
else
res = r(500, gzbody.to_s, env)
end
end
end
res
rescue Errno::ENOENT # could get here from a race
r(404)
rescue Errno::EACCES # could get here from a race
r(403)
rescue => e
r(500, e, env)
end
def r(code, exc = nil, env = nil)
if env && exc && logger = env["rack.logger"]
msg = exc.message if exc.respond_to?(:message)
msg = msg.dump if /[[:cntrl:]]/ =~ msg # prevent code injection
logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
"#{code} #{msg}")
if exc.respond_to?(:backtrace) && !(SystemCallError === exc)
exc.backtrace.each { |line| logger.warn(line) }
end
end
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
[ code, {}, [] ]
else
msg = "#{code} #{Rack::Utils::HTTP_STATUS_CODES[code.to_i]}\n"
h = { 'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s }
[ code, h, [ msg ] ]
end
end
end
|