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
| | # -*- encoding: binary -*-
# Copyright (C) 2013-2018 all contributors <yahns-public@yhbt.net>
# License: GPL-2.0+ <https://www.gnu.org/licenses/gpl-2.0.txt>
# frozen_string_literal: true
#
# if running under yahns, worker_processes is recommended to avoid conflicting
# with the SIGCHLD handler in yahns.
# Be careful if using Rack::Deflater, this needs the following commit
# (currently in rack.git, not yet in 1.5.2):
# commit 7bda8d485b38403bf07f43793d37b66b7a8281d6
# (delfater: ensure that parent body is always closed)
# Otherwise you will get zombies from HEAD requests which accept compressed
# responses.
#
# Usage in config.ru using cgit as an example:
#
# use Rack::Chunked
# # other Rack middlewares can go here...
#
# # cgit: https://git.zx2c4.com/cgit/
# run ExecCgi.new('/path/to/cgit.cgi', opts)
#
class ExecCgi
class MyIO
attr_writer :my_pid
attr_writer :body_tip
attr_reader :rd
def initialize(rd)
@rd = rd
end
def each
buf = @body_tip
yield buf unless buf.empty?
case tmp = @rd.read_nonblock(8192, buf, exception: false)
when :wait_readable
@rd.wait_readable
when nil
break
else # String
yield tmp
end while true
self
ensure
# do this sooner, since the response body may be buffered, we want
# to release our FD as soon as possible.
close
end
def close
# yahns will call this again after its done writing the response
# body, so we must ensure its idempotent.
# Note: this object (and any client-specific objects) will never
# be shared across different threads, so we do not need extra
# mutual exclusion here.
return if @rd.closed?
@rd.close
begin
Process.waitpid(@my_pid)
rescue Errno::ECHILD
end if defined?(@my_pid) && @my_pid
end
end
PASS_VARS = %w(
CONTENT_LENGTH
CONTENT_TYPE
AUTH_TYPE
PATH_INFO
PATH_TRANSLATED
QUERY_STRING
REMOTE_ADDR
REMOTE_HOST
REMOTE_IDENT
REMOTE_USER
REQUEST_METHOD
SERVER_NAME
SERVER_PORT
SERVER_PROTOCOL
SERVER_SOFTWARE
SCRIPT_NAME
).map(&:freeze) # frozen strings are faster for Hash assignments
def initialize(*args)
@env = Hash === args[0] ? args.shift : {}
@args = args
first = args[0] or
raise ArgumentError, "need path to executable"
first[0] == ?/ or args[0] = ::File.expand_path(first)
File.executable?(args[0]) or
raise ArgumentError, "#{args[0]} is not executable"
@opts = Hash === args[-1] ? args.pop : {}
end
# Calls the app
def call(env)
env.delete('HTTP_PROXY') # ref: https://httpoxy.org/
cgi_env = { "GATEWAY_INTERFACE" => "CGI/1.1" }
PASS_VARS.each { |key| val = env[key] and cgi_env[key] = val }
env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }
rd, wr = IO.pipe
io = MyIO.new(rd)
errbody = io
errbody.my_pid = spawn(cgi_env.merge!(@env), *@args,
@opts.merge(out: wr, close_others: true))
wr.close
begin
head = rd.readpartial(8192)
until head =~ /\r?\n\r?\n/
tmp = rd.readpartial(8192)
head << tmp
tmp.clear
end
head, body = head.split(/\r?\n\r?\n/, 2)
io.body_tip = body
env["HTTP_VERSION"] ||= "HTTP/1.0" # stop Rack::Chunked for HTTP/0.9
headers = Rack::Utils::HeaderHash.new
prev = nil
head.split(/\r?\n/).each do |line|
case line
when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
end
end
status = headers.delete("Status") || 200
errbody = nil
[ status, headers, io ]
rescue EOFError
[ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
end
ensure
errbody.close if errbody
end
end
|