diff options
Diffstat (limited to 'extras/exec_cgi.rb')
-rw-r--r-- | extras/exec_cgi.rb | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/extras/exec_cgi.rb b/extras/exec_cgi.rb new file mode 100644 index 0000000..083047e --- /dev/null +++ b/extras/exec_cgi.rb @@ -0,0 +1,108 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors +# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt) +class ExecCgi + class MyIO < Kgio::Pipe + attr_writer :my_pid + attr_writer :body_tip + attr_writer :chunked + + def each + buf = @body_tip || "" + if buf.size > 0 + buf = "#{buf.size.to_s(16)}\r\n#{buf}\r\n" if @chunked + yield buf + end + while tmp = kgio_read(8192, buf) + tmp = "#{tmp.size.to_s(16)}\r\n#{tmp}\r\n" if @chunked + yield tmp + end + yield("0\r\n\r\n") if @chunked + self + end + + def close + super + if defined?(@my_pid) && @my_pid + begin + Process.waitpid(@my_pid) + rescue Errno::ECHILD + end + end + nil + end + end + + PASS_VARS = %w( + CONTENT_LENGTH + CONTENT_TYPE + GATEWAY_INTERFACE + 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 + ).map(&:freeze) # frozen strings are faster for Hash assignments + + def initialize(*args) + @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 = { "SCRIPT_NAME" => @args[0], "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 + pipe[0].my_pid = Process.spawn(cgi_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/) + pipe.body_tip = body + pipe.chunked = false + + 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 + unless headers.include?("Content-Length") || + headers.include?("Transfer-Encoding") + case env['HTTP_VERSION'] + when 'HTTP/1.0', nil + # server will drop connection anyways + else + headers["Transfer-Encoding"] = "chunked" + pipe.chunked = true + end + end + [ status, headers, pipe ] + else + [ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ] + end + end +end |