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
| | # -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
# License: GPLv2 or later (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...
#
# run ExecCgi.new('/path/to/cgit.cgi') # ref: https://git.zx2c4.com/cgit/
#
class ExecCgi
class MyIO < Kgio::Pipe
attr_writer :my_pid
attr_writer :body_tip
def each
buf = @body_tip || ''.dup
if buf.size > 0
yield buf
end
while tmp = kgio_read(8192, buf)
yield tmp
end
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 closed?
super
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"
end
# Calls the app
def call(env)
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_/ }
pipe = MyIO.pipe
errbody = pipe[0]
errbody.my_pid = Process.spawn(cgi_env.merge!(@env), *@args,
out: pipe[1], close_others: true)
pipe[1].close
pipe = pipe[0]
if head = pipe.kgio_read(8192)
until head =~ /\r?\n\r?\n/
tmp = pipe.kgio_read(8192) or break
head << tmp
end
head, body = head.split(/\r?\n\r?\n/, 2)
pipe.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, pipe ]
else
[ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
end
ensure
errbody.close if errbody
end
end
|