# -*- encoding: binary -*- # Copyright (C) 2013, Eric Wong 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