yahns 0.0.1 → 0.0.2
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 +4 -4
- data/.gitignore +1 -0
- data/Documentation/.gitignore +5 -0
- data/Documentation/GNUmakefile +50 -0
- data/Documentation/yahns-rackup.txt +152 -0
- data/Documentation/yahns.txt +68 -0
- data/Documentation/yahns_config.txt +563 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +14 -7
- data/HACKING +56 -0
- data/INSTALL +8 -0
- data/README +15 -2
- data/Rakefile +2 -2
- data/bin/yahns +1 -2
- data/bin/yahns-rackup +9 -0
- data/examples/yahns_multi.conf.rb +14 -4
- data/examples/yahns_rack_basic.conf.rb +17 -1
- data/extras/README +16 -0
- data/extras/autoindex.rb +151 -0
- data/extras/exec_cgi.rb +108 -0
- data/extras/proxy_pass.rb +210 -0
- data/extras/try_gzip_static.rb +208 -0
- data/lib/yahns.rb +5 -2
- data/lib/yahns/acceptor.rb +64 -22
- data/lib/yahns/cap_input.rb +2 -2
- data/lib/yahns/{client_expire_portable.rb → client_expire_generic.rb} +12 -11
- data/lib/yahns/{client_expire.rb → client_expire_tcpi.rb} +7 -6
- data/lib/yahns/config.rb +107 -22
- data/lib/yahns/daemon.rb +2 -0
- data/lib/yahns/fdmap.rb +28 -9
- data/lib/yahns/http_client.rb +123 -37
- data/lib/yahns/http_context.rb +21 -3
- data/lib/yahns/http_response.rb +80 -19
- data/lib/yahns/log.rb +23 -4
- data/lib/yahns/queue_epoll.rb +20 -9
- data/lib/yahns/queue_quitter.rb +16 -0
- data/lib/yahns/queue_quitter_pipe.rb +24 -0
- data/lib/yahns/rack.rb +0 -1
- data/lib/yahns/rackup_handler.rb +57 -0
- data/lib/yahns/server.rb +189 -59
- data/lib/yahns/server_mp.rb +43 -35
- data/lib/yahns/sigevent_pipe.rb +1 -0
- data/lib/yahns/socket_helper.rb +37 -11
- data/lib/yahns/stream_file.rb +14 -4
- data/lib/yahns/stream_input.rb +13 -7
- data/lib/yahns/tcp_server.rb +7 -0
- data/lib/yahns/tmpio.rb +10 -3
- data/lib/yahns/unix_server.rb +7 -0
- data/lib/yahns/wbuf.rb +19 -2
- data/lib/yahns/wbuf_common.rb +10 -3
- data/lib/yahns/wbuf_str.rb +24 -0
- data/lib/yahns/worker.rb +5 -26
- data/test/helper.rb +15 -5
- data/test/server_helper.rb +37 -1
- data/test/test_bin.rb +17 -8
- data/test/test_buffer_tmpdir.rb +103 -0
- data/test/test_client_expire.rb +71 -35
- data/test/test_client_max_body_size.rb +5 -13
- data/test/test_config.rb +1 -1
- data/test/test_expect_100.rb +176 -0
- data/test/test_extras_autoindex.rb +53 -0
- data/test/test_extras_exec_cgi.rb +81 -0
- data/test/test_extras_exec_cgi.sh +35 -0
- data/test/test_extras_try_gzip_static.rb +177 -0
- data/test/test_input.rb +128 -0
- data/test/test_mt_accept.rb +48 -0
- data/test/test_output_buffering.rb +90 -63
- data/test/test_rack.rb +1 -1
- data/test/test_rack_hijack.rb +2 -6
- data/test/test_reopen_logs.rb +2 -8
- data/test/test_serve_static.rb +104 -8
- data/test/test_server.rb +448 -73
- data/test/test_stream_file.rb +1 -1
- data/test/test_unix_socket.rb +72 -0
- data/test/test_wbuf.rb +20 -17
- data/yahns.gemspec +3 -0
- metadata +57 -5
data/extras/exec_cgi.rb
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# -*- encoding: binary -*-
|
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
|
3
|
+
# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
|
|
4
|
+
class ExecCgi
|
|
5
|
+
class MyIO < Kgio::Pipe
|
|
6
|
+
attr_writer :my_pid
|
|
7
|
+
attr_writer :body_tip
|
|
8
|
+
attr_writer :chunked
|
|
9
|
+
|
|
10
|
+
def each
|
|
11
|
+
buf = @body_tip || ""
|
|
12
|
+
if buf.size > 0
|
|
13
|
+
buf = "#{buf.size.to_s(16)}\r\n#{buf}\r\n" if @chunked
|
|
14
|
+
yield buf
|
|
15
|
+
end
|
|
16
|
+
while tmp = kgio_read(8192, buf)
|
|
17
|
+
tmp = "#{tmp.size.to_s(16)}\r\n#{tmp}\r\n" if @chunked
|
|
18
|
+
yield tmp
|
|
19
|
+
end
|
|
20
|
+
yield("0\r\n\r\n") if @chunked
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def close
|
|
25
|
+
super
|
|
26
|
+
if defined?(@my_pid) && @my_pid
|
|
27
|
+
begin
|
|
28
|
+
Process.waitpid(@my_pid)
|
|
29
|
+
rescue Errno::ECHILD
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
PASS_VARS = %w(
|
|
37
|
+
CONTENT_LENGTH
|
|
38
|
+
CONTENT_TYPE
|
|
39
|
+
GATEWAY_INTERFACE
|
|
40
|
+
AUTH_TYPE
|
|
41
|
+
PATH_INFO
|
|
42
|
+
PATH_TRANSLATED
|
|
43
|
+
QUERY_STRING
|
|
44
|
+
REMOTE_ADDR
|
|
45
|
+
REMOTE_HOST
|
|
46
|
+
REMOTE_IDENT
|
|
47
|
+
REMOTE_USER
|
|
48
|
+
REQUEST_METHOD
|
|
49
|
+
SERVER_NAME
|
|
50
|
+
SERVER_PORT
|
|
51
|
+
SERVER_PROTOCOL
|
|
52
|
+
SERVER_SOFTWARE
|
|
53
|
+
).map(&:freeze) # frozen strings are faster for Hash assignments
|
|
54
|
+
|
|
55
|
+
def initialize(*args)
|
|
56
|
+
@args = args
|
|
57
|
+
first = args[0] or
|
|
58
|
+
raise ArgumentError, "need path to executable"
|
|
59
|
+
first[0] == ?/ or args[0] = ::File.expand_path(first)
|
|
60
|
+
File.executable?(args[0]) or
|
|
61
|
+
raise ArgumentError, "#{args[0]} is not executable"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Calls the app
|
|
65
|
+
def call(env)
|
|
66
|
+
cgi_env = { "SCRIPT_NAME" => @args[0], "GATEWAY_INTERFACE" => "CGI/1.1" }
|
|
67
|
+
PASS_VARS.each { |key| val = env[key] and cgi_env[key] = val }
|
|
68
|
+
env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }
|
|
69
|
+
pipe = MyIO.pipe
|
|
70
|
+
pipe[0].my_pid = Process.spawn(cgi_env, *@args,
|
|
71
|
+
out: pipe[1], close_others: true)
|
|
72
|
+
pipe[1].close
|
|
73
|
+
pipe = pipe[0]
|
|
74
|
+
|
|
75
|
+
if head = pipe.kgio_read(8192)
|
|
76
|
+
until head =~ /\r?\n\r?\n/
|
|
77
|
+
tmp = pipe.kgio_read(8192) or break
|
|
78
|
+
head << tmp
|
|
79
|
+
end
|
|
80
|
+
head, body = head.split(/\r?\n\r?\n/)
|
|
81
|
+
pipe.body_tip = body
|
|
82
|
+
pipe.chunked = false
|
|
83
|
+
|
|
84
|
+
headers = Rack::Utils::HeaderHash.new
|
|
85
|
+
prev = nil
|
|
86
|
+
head.split(/\r?\n/).each do |line|
|
|
87
|
+
case line
|
|
88
|
+
when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
|
|
89
|
+
when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
status = headers.delete("Status") || 200
|
|
93
|
+
unless headers.include?("Content-Length") ||
|
|
94
|
+
headers.include?("Transfer-Encoding")
|
|
95
|
+
case env['HTTP_VERSION']
|
|
96
|
+
when 'HTTP/1.0', nil
|
|
97
|
+
# server will drop connection anyways
|
|
98
|
+
else
|
|
99
|
+
headers["Transfer-Encoding"] = "chunked"
|
|
100
|
+
pipe.chunked = true
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
[ status, headers, pipe ]
|
|
104
|
+
else
|
|
105
|
+
[ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# -*- encoding: binary -*-
|
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'kgio'
|
|
7
|
+
require 'kcar' # gem install kcar
|
|
8
|
+
require 'rack/request'
|
|
9
|
+
require 'thread'
|
|
10
|
+
require 'timeout'
|
|
11
|
+
|
|
12
|
+
# Totally synchronous and Rack 1.1-compatible, this will probably be rewritten.
|
|
13
|
+
# to take advantage of rack.hijack and use the non-blocking I/O facilities
|
|
14
|
+
# in yahns. yahns may have to grow a supported API for that...
|
|
15
|
+
# For now, we this blocks a worker thread; fortunately threads are reasonably
|
|
16
|
+
# cheap on GNU/Linux...
|
|
17
|
+
# This is totally untested but currently doesn't serve anything important.
|
|
18
|
+
class ProxyPass # :nodoc:
|
|
19
|
+
CHUNK_SIZE = 16384
|
|
20
|
+
ERROR_502 = [ 502, {'Content-Length'=>'0','Content-Type'=>'text/plain'}, [] ]
|
|
21
|
+
|
|
22
|
+
class ConnPool
|
|
23
|
+
def initialize
|
|
24
|
+
@mtx = Mutex.new
|
|
25
|
+
@objs = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get
|
|
29
|
+
@mtx.synchronize { @objs.pop }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def put(obj)
|
|
33
|
+
@mtx.synchronize { @objs << obj }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class UpstreamSocket < Kgio::Socket # :nodoc:
|
|
38
|
+
attr_writer :expiry
|
|
39
|
+
|
|
40
|
+
# called automatically by kgio_read!
|
|
41
|
+
def kgio_wait_readable(timeout = nil)
|
|
42
|
+
super(timeout || wait_time)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def wait_time
|
|
46
|
+
tout = @expiry ? @expiry - Time.now : @timeout
|
|
47
|
+
raise Timeout::Error, "request timed out", [] if tout < 0
|
|
48
|
+
tout
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def readpartial(bytes, buf = Thread.current[:proxy_pass_buf] ||= "")
|
|
52
|
+
case rv = kgio_read!(bytes, buf)
|
|
53
|
+
when String
|
|
54
|
+
@expiry += @timeout # bump expiry when we succeed
|
|
55
|
+
end
|
|
56
|
+
rv
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def req_write(buf, timeout)
|
|
60
|
+
@timeout = timeout
|
|
61
|
+
@expiry = Time.now + timeout
|
|
62
|
+
case rv = kgio_trywrite(buf)
|
|
63
|
+
when :wait_writable
|
|
64
|
+
kgio_wait_writable(wait_time)
|
|
65
|
+
when nil
|
|
66
|
+
return
|
|
67
|
+
when String
|
|
68
|
+
buf = rv
|
|
69
|
+
end while true
|
|
70
|
+
end
|
|
71
|
+
end # class UpstreamSocket
|
|
72
|
+
|
|
73
|
+
class UpstreamResponse < Kcar::Response # :nodoc:
|
|
74
|
+
# Called by the Rack server at the end of a successful response
|
|
75
|
+
def close
|
|
76
|
+
reusable = @parser.keepalive? && @parser.body_eof?
|
|
77
|
+
super
|
|
78
|
+
@pool.put(self) if reusable
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# req is just a string buffer of HTTP headers
|
|
83
|
+
def req_write(req, timeout)
|
|
84
|
+
@sock.req_write(req, timeout)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# returns true if the socket is still alive, nil if dead
|
|
88
|
+
def sock_alive?
|
|
89
|
+
@reused = (:wait_readable == (@sock.kgio_tryread(1) rescue nil)) ?
|
|
90
|
+
true : @sock.close
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# returns true if the socket was reused and thus retryable
|
|
94
|
+
def fail_retryable?
|
|
95
|
+
@sock.close
|
|
96
|
+
@reused
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initialize(sock, pool)
|
|
100
|
+
super(sock)
|
|
101
|
+
@reused = false
|
|
102
|
+
@pool = pool
|
|
103
|
+
end
|
|
104
|
+
end # class UpstreamResponse
|
|
105
|
+
|
|
106
|
+
# take a responder from the pool, we'll add the object back to the
|
|
107
|
+
# pool in UpstreamResponse#close
|
|
108
|
+
def responder_get
|
|
109
|
+
while obj = @pool.get
|
|
110
|
+
return obj if obj.sock_alive?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
UpstreamResponse.new(UpstreamSocket.start(@sockaddr), @pool)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def initialize(dest, timeout = 5)
|
|
117
|
+
case dest
|
|
118
|
+
when %r{\Ahttp://([^/]+)(/.*)\z}
|
|
119
|
+
path = $2
|
|
120
|
+
host, port = $1.split(/:/)
|
|
121
|
+
@sockaddr = Socket.sockaddr_in(port || 80, host)
|
|
122
|
+
|
|
123
|
+
# methods from Rack::Request we want:
|
|
124
|
+
allow = %w(fullpath host_with_port host port url path)
|
|
125
|
+
@path = path
|
|
126
|
+
want = path.scan(/\$(\w+)/).flatten! || []
|
|
127
|
+
diff = want - allow
|
|
128
|
+
diff.empty? or
|
|
129
|
+
raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
|
|
130
|
+
else
|
|
131
|
+
raise ArgumentError, "destination must be an HTTP URL"
|
|
132
|
+
end
|
|
133
|
+
@pool = ConnPool.new
|
|
134
|
+
@timeout = timeout
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def call(env)
|
|
138
|
+
case request_method = env["REQUEST_METHOD"]
|
|
139
|
+
when "GET", "HEAD" # OK
|
|
140
|
+
else
|
|
141
|
+
return [ 405, [%w(Content-Length 0), %w(Content-Length 0)], [] ]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
req = Rack::Request.new(env)
|
|
145
|
+
path = @path.gsub(/\$(\w+)/) { req.__send__($1.to_sym) }
|
|
146
|
+
req = "#{request_method} #{path} HTTP/1.1\r\n" \
|
|
147
|
+
"X-Forwarded-For: #{env["REMOTE_ADDR"]}\r\n"
|
|
148
|
+
|
|
149
|
+
# pass most HTTP_* headers through as-is
|
|
150
|
+
chunked = false
|
|
151
|
+
env.each do |key, val|
|
|
152
|
+
%r{\AHTTP_(\w+)\z} =~ key or next
|
|
153
|
+
key = $1
|
|
154
|
+
next if %r{\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR)} =~ key
|
|
155
|
+
chunked = true if %r{\ATRANSFER_ENCODING} =~ key && val =~ /\bchunked\b/i
|
|
156
|
+
key.tr!("_", "-")
|
|
157
|
+
req << "#{key}: #{val}\r\n"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# special cases which Rack does not prefix:
|
|
161
|
+
ctype = env["CONTENT_TYPE"] and req << "Content-Type: #{ctype}\r\n"
|
|
162
|
+
clen = env["CONTENT_LENGTH"] and req << "Content-Length: #{clen}\r\n"
|
|
163
|
+
req << "\r\n"
|
|
164
|
+
|
|
165
|
+
# get an open socket and send the headers
|
|
166
|
+
ures = responder_get
|
|
167
|
+
ures.req_write(req, @timeout)
|
|
168
|
+
|
|
169
|
+
# send the request body if there was one
|
|
170
|
+
send_body(env["rack.input"], ures, chunked) if chunked || clen
|
|
171
|
+
|
|
172
|
+
# wait for the response here
|
|
173
|
+
status, header, body = res = ures.rack
|
|
174
|
+
|
|
175
|
+
# don't let the upstream Connection and Keep-Alive headers leak through
|
|
176
|
+
header.delete_if do |k,_|
|
|
177
|
+
k =~ /\A(?:Connection|Keep-Alive)\z/i
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
case request_method
|
|
181
|
+
when "HEAD"
|
|
182
|
+
# kcar doesn't know if it's a HEAD or GET response, and HEAD
|
|
183
|
+
# responses have Content-Length in it which fools kcar...
|
|
184
|
+
body.parser.body_bytes_left = 0
|
|
185
|
+
res[1] = header.dup
|
|
186
|
+
body.close # clobbers original header
|
|
187
|
+
res[2] = body = []
|
|
188
|
+
end
|
|
189
|
+
res
|
|
190
|
+
rescue => e
|
|
191
|
+
retry if ures && ures.fail_retryable? && request_method != "POST"
|
|
192
|
+
ERROR_502
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def send_body(input, ures, chunked)
|
|
196
|
+
buf = Thread.current[:proxy_pass_buf] ||= ""
|
|
197
|
+
|
|
198
|
+
if chunked # unlikely
|
|
199
|
+
while input.read(16384, buf)
|
|
200
|
+
buf.replace("#{buf.size.to_s(16)}\r\n#{buf}\r\n")
|
|
201
|
+
ures.req_write(buf, @timeout)
|
|
202
|
+
end
|
|
203
|
+
ures.req_write("0\r\n\r\n")
|
|
204
|
+
else # common if we hit uploads
|
|
205
|
+
while input.read(16384, buf)
|
|
206
|
+
ures.req_write(buf, @timeout)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
|
2
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'rack/utils'
|
|
5
|
+
require 'rack/mime'
|
|
6
|
+
require 'kgio'
|
|
7
|
+
|
|
8
|
+
class TryGzipStatic
|
|
9
|
+
attr_accessor :root
|
|
10
|
+
class KF < Kgio::File
|
|
11
|
+
# attr_writer :sf_range
|
|
12
|
+
|
|
13
|
+
# only used if the server does not handle #to_path,
|
|
14
|
+
# yahns should never hit this
|
|
15
|
+
def each
|
|
16
|
+
raise "we should never get here in yahns"
|
|
17
|
+
buf = ""
|
|
18
|
+
rsize = 8192
|
|
19
|
+
if @sf_range
|
|
20
|
+
file.seek(@sf_range.begin)
|
|
21
|
+
sf_count = @sf_range.end - @sf_range.begin + 1
|
|
22
|
+
while sf_count > 0
|
|
23
|
+
read(sf_count > rsize ? rsize : sf_count, buf) or break
|
|
24
|
+
sf_count -= buf.size
|
|
25
|
+
yield buf
|
|
26
|
+
end
|
|
27
|
+
raise "file truncated" if sf_count != 0
|
|
28
|
+
else
|
|
29
|
+
yield(buf) while read(rsize, buf)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(root, default_type = 'text/plain')
|
|
35
|
+
@root = root
|
|
36
|
+
@default_type = default_type
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fspath(env)
|
|
40
|
+
path_info = Rack::Utils.unescape(env["PATH_INFO"])
|
|
41
|
+
path_info =~ /\.\./ ? nil : "#@root#{path_info}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_range(env, path, st)
|
|
45
|
+
if ims = env["HTTP_IF_MODIFIED_SINCE"]
|
|
46
|
+
return [ 304, {}, [] ] if st.mtime.httpdate == ims
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
size = st.size
|
|
50
|
+
ranges = Rack::Utils.byte_ranges(env, size)
|
|
51
|
+
if ranges.nil? || ranges.length > 1
|
|
52
|
+
[ 200 ] # serve the whole thing, possibly with static gzip \o/
|
|
53
|
+
elsif ranges.empty?
|
|
54
|
+
res = r(416)
|
|
55
|
+
res[1]["Content-Range"] = "bytes */#{size}"
|
|
56
|
+
res
|
|
57
|
+
else # partial response, no using static gzip file
|
|
58
|
+
range = ranges[0]
|
|
59
|
+
len = range.end - range.begin + 1
|
|
60
|
+
h = fheader(env, path, st, nil, len)
|
|
61
|
+
h["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
|
|
62
|
+
[ 206, h, range ]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fheader(env, path, st, gz_st = nil, len = nil)
|
|
67
|
+
if path =~ /(.[^.]+)\z/
|
|
68
|
+
mime = Rack::Mime.mime_type($1, @default_type)
|
|
69
|
+
else
|
|
70
|
+
mime = @default_type
|
|
71
|
+
end
|
|
72
|
+
len ||= (gz_st ? gz_st : st).size
|
|
73
|
+
h = {
|
|
74
|
+
"Content-Type" => mime,
|
|
75
|
+
"Content-Length" => len.to_s,
|
|
76
|
+
"Last-Modified" => st.mtime.httpdate,
|
|
77
|
+
"Accept-Ranges" => "bytes",
|
|
78
|
+
}
|
|
79
|
+
h["Content-Encoding"] = "gzip" if gz_st
|
|
80
|
+
h
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def head_no_gz(res, env, path, st)
|
|
84
|
+
res[1] = fheader(env, path, st)
|
|
85
|
+
res[2] = [] # empty body
|
|
86
|
+
res
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def stat_path(env)
|
|
90
|
+
path = fspath(env) or return r(403)
|
|
91
|
+
begin
|
|
92
|
+
st = File.stat(path)
|
|
93
|
+
st.file? ? [ path, st ] : r(404)
|
|
94
|
+
rescue Errno::ENOENT
|
|
95
|
+
r(404)
|
|
96
|
+
rescue Errno::EACCES
|
|
97
|
+
r(403)
|
|
98
|
+
rescue => e
|
|
99
|
+
r(500, e.message, env)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def head(env)
|
|
104
|
+
path, st = res = stat_path(env)
|
|
105
|
+
return res if Integer === path # integer status code on failure
|
|
106
|
+
|
|
107
|
+
# see if it's a range request, no gzipped version if so
|
|
108
|
+
status, _ = res = get_range(env, path, st)
|
|
109
|
+
case status
|
|
110
|
+
when 206
|
|
111
|
+
res[2] = [] # empty body, headers are all set
|
|
112
|
+
res
|
|
113
|
+
when 200 # fall through to trying gzipped version
|
|
114
|
+
# client requested gzipped path explicitly or did not want gzip
|
|
115
|
+
if path =~ /\.gz\z/i || !want_gzip?(env)
|
|
116
|
+
head_no_gz(res, env, path, st)
|
|
117
|
+
else # try the gzipped version
|
|
118
|
+
begin
|
|
119
|
+
gz_st = File.stat("#{path}.gz")
|
|
120
|
+
if gz_st.mtime == st.mtime
|
|
121
|
+
res[1] = fheader(env, path, st, gz_st)
|
|
122
|
+
res[2] = []
|
|
123
|
+
res
|
|
124
|
+
else
|
|
125
|
+
head_no_gz(res, env, path, st)
|
|
126
|
+
end
|
|
127
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
128
|
+
head_no_gz(res, env, path, st)
|
|
129
|
+
rescue => e
|
|
130
|
+
r(500, e.message, env)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
else # 416, 304
|
|
134
|
+
res
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def call(env)
|
|
139
|
+
case env["REQUEST_METHOD"]
|
|
140
|
+
when "GET" then get(env)
|
|
141
|
+
when "HEAD" then head(env)
|
|
142
|
+
else r(405)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def want_gzip?(env)
|
|
147
|
+
env["HTTP_ACCEPT_ENCODING"] =~ /\bgzip\b/i
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def get(env)
|
|
151
|
+
path, st, _ = res = stat_path(env)
|
|
152
|
+
return res if Integer === path # integer status code on failure
|
|
153
|
+
|
|
154
|
+
# see if it's a range request, no gzipped version if so
|
|
155
|
+
status, _, _ = res = get_range(env, path, st)
|
|
156
|
+
case status
|
|
157
|
+
when 206
|
|
158
|
+
res[2] = KF.open(path) # stat succeeded
|
|
159
|
+
when 200
|
|
160
|
+
# client requested gzipped path explicitly or did not want gzip
|
|
161
|
+
if path =~ /\.gz\z/i || !want_gzip?(env)
|
|
162
|
+
res[1] = fheader(env, path, st)
|
|
163
|
+
res[2] = KF.open(path)
|
|
164
|
+
else
|
|
165
|
+
case gzbody = KF.tryopen("#{path}.gz")
|
|
166
|
+
when KF
|
|
167
|
+
gz_st = gzbody.stat
|
|
168
|
+
if gz_st.file? && gz_st.mtime == st.mtime
|
|
169
|
+
# yay! serve the gzipped version as the regular one
|
|
170
|
+
# this should be the most likely code path
|
|
171
|
+
res[1] = fheader(env, path, st, gz_st)
|
|
172
|
+
res[2] = gzbody
|
|
173
|
+
else
|
|
174
|
+
gzbody.close
|
|
175
|
+
res[1] = fheader(env, path, st)
|
|
176
|
+
res[2] = KF.open(path)
|
|
177
|
+
end
|
|
178
|
+
when :ENOENT, :EACCES
|
|
179
|
+
res[1] = fheader(env, path, st)
|
|
180
|
+
res[2] = KF.open(path)
|
|
181
|
+
else
|
|
182
|
+
res = r(500, gzbody.to_s, env)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
res
|
|
187
|
+
rescue Errno::ENOENT # could get here from a race
|
|
188
|
+
r(404)
|
|
189
|
+
rescue Errno::EACCES # could get here from a race
|
|
190
|
+
r(403)
|
|
191
|
+
rescue => e
|
|
192
|
+
r(500, e.message, env)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def r(code, msg = nil, env = nil)
|
|
196
|
+
if env && logger = env["rack.logger"]
|
|
197
|
+
logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
|
|
198
|
+
"#{code} #{msg.inspect}")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
|
|
202
|
+
[ code, {}, [] ]
|
|
203
|
+
else
|
|
204
|
+
h = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }
|
|
205
|
+
[ code, h, [] ]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|