about summary refs log tree commit homepage
path: root/extras/exec_cgi.rb
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2013-11-02 10:54:17 +0000
committerEric Wong <normalperson@yhbt.net>2013-11-02 22:03:54 +0000
commit3192ec1b4054bcc228dfb88e57d5e1c828682a7b (patch)
treee7f7ac214f4a6e4ccdc1f1c109f49b555921a881 /extras/exec_cgi.rb
parentf78020396ac822c31f7f0b1a593bd3f58362a27a (diff)
downloadyahns-3192ec1b4054bcc228dfb88e57d5e1c828682a7b.tar.gz
These applications are what I'll be using to run on yahns on
my personal server.

Including them here will be helpful for me to find bugs.  I've
already found some, the following commits were directly the result
of playing with these extras:

* stream_file: only close FDs we opened ourselves
* worker-less server should not waitpid indiscriminately
* http: do not drop Content-Range from response headers
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