about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-03-20 01:28:07 -0700
committerEric Wong <normalperson@yhbt.net>2009-03-20 02:32:49 -0700
commitb3ab233fa8fd8de28450bb9d799a9b568abb324d (patch)
tree047eeba1eb3e7f6a6739d1dff5125e7b76b7d56f
parent508eac750a8c278f5a965e6f4ac0f8a90677f1a2 (diff)
downloadunicorn-b3ab233fa8fd8de28450bb9d799a9b568abb324d.tar.gz
This is a Rack handler that passes Rack::Lint running cgit
and so it has been lightly tested.  No other CGI executables
have been run with it.
-rw-r--r--Manifest1
-rw-r--r--lib/unicorn/app/exec_cgi.rb150
2 files changed, 151 insertions, 0 deletions
diff --git a/Manifest b/Manifest
index 729d33b..cca63ab 100644
--- a/Manifest
+++ b/Manifest
@@ -20,6 +20,7 @@ ext/unicorn/http11/http11_parser.h
 ext/unicorn/http11/http11_parser.rl
 ext/unicorn/http11/http11_parser_common.rl
 lib/unicorn.rb
+lib/unicorn/app/exec_cgi.rb
 lib/unicorn/configurator.rb
 lib/unicorn/const.rb
 lib/unicorn/http_request.rb
diff --git a/lib/unicorn/app/exec_cgi.rb b/lib/unicorn/app/exec_cgi.rb
new file mode 100644
index 0000000..f5e7db9
--- /dev/null
+++ b/lib/unicorn/app/exec_cgi.rb
@@ -0,0 +1,150 @@
+require 'unicorn'
+require 'rack'
+
+module Unicorn::App
+
+  # This class is highly experimental (even more so than the rest of Unicorn)
+  # and has never run anything other than cgit.
+  class ExecCgi
+
+    CHUNK_SIZE = 16384
+    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 { |x| x.freeze }.freeze # frozen strings are faster for Hash lookups
+
+    # Intializes the app, example of usage in a config.ru
+    #   map "/cgit" do
+    #     run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
+    #   end
+    def initialize(*args)
+      @args = args.dup
+      first = @args[0] or
+        raise ArgumentError, "need path to executable"
+      first[0..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)
+      out, err = Tempfile.new(''), Tempfile.new('')
+      out.unlink
+      err.unlink
+      inp = force_file_input(env)
+      inp.sync = out.sync = err.sync = true
+      pid = fork { run_child(inp, out, err, env) }
+      inp.close
+      pid, status = Process.waitpid2(pid)
+      write_errors(env, err, status) if err.stat.size > 0
+      err.close
+
+      return parse_output!(out) if status.success?
+      out.close
+      [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
+    end
+
+    private
+
+    def run_child(inp, out, err, env)
+      PASS_VARS.each do |key|
+        val = env[key] or next
+        ENV[key] = val
+      end
+      ENV['SCRIPT_NAME'] = @args[0]
+      ENV['GATEWAY_INTERFACE'] = 'CGI/1.1'
+      env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] }
+
+      IO.new(0).reopen(inp)
+      IO.new(1).reopen(out)
+      IO.new(2).reopen(err)
+      exec(*@args)
+    end
+
+    # Extracts headers from CGI out, will change the offset of out.
+    # This returns a standard Rack-compatible return value:
+    #   [ 200, HeadersHash, body ]
+    def parse_output!(out)
+      size = out.stat.size
+      out.sysseek(0)
+      head = out.sysread(CHUNK_SIZE)
+      offset = 2
+      head, body = head.split(/\n\n/, 2)
+      if body.nil?
+        head, body = head.split(/\r\n\r\n/, 2)
+        offset = 4
+      end
+      offset += head.length
+      out.instance_variable_set('@unicorn_app_exec_cgi_offset', offset)
+      size -= offset
+
+      # Allows +out+ to be used as a Rack body.
+      def out.each
+        sysseek(@unicorn_app_exec_cgi_offset)
+        begin
+          loop { yield(sysread(CHUNK_SIZE)) }
+        rescue EOFError
+        end
+      end
+
+      prev = nil
+      headers = Rack::Utils::HeaderHash.new
+      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
+      headers['Content-Length'] = size.to_s
+      [ 200, headers, out ]
+    end
+
+    # ensures rack.input is a file handle that we can redirect stdin to
+    def force_file_input(env)
+      inp = env['rack.input']
+      if inp.respond_to?(:fileno) && Integer === inp.fileno
+        inp
+      elsif inp.size == 0 # inp could be a StringIO or StringIO-like object
+        ::File.open('/dev/null')
+      else
+        tmp = Tempfile.new('')
+        tmp.unlink
+        tmp.binmode
+
+        # Rack::Lint::InputWrapper doesn't allow sysread :(
+        while buf = inp.read(CHUNK_SIZE)
+          tmp.syswrite(buf)
+        end
+        tmp.sysseek(0)
+        tmp
+      end
+    end
+
+    # rack.errors this may not be an IO object, so we couldn't
+    # just redirect the CGI executable to that earlier.
+    def write_errors(env, err, status)
+      err.seek(0)
+      dst = env['rack.errors']
+      pid = status.pid
+      dst.write("#{pid}: #{@args.inspect} status=#{status} stderr:\n")
+      err.each_line { |line| dst.write("#{pid}: #{line}") }
+      dst.flush
+    end
+
+  end
+
+end