about summary refs log tree commit homepage
path: root/extras/exec_cgi.rb
diff options
context:
space:
mode:
Diffstat (limited to 'extras/exec_cgi.rb')
-rw-r--r--extras/exec_cgi.rb108
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