yahns Ruby server user/dev discussion
 help / color / mirror / code / Atom feed
blob 2746c090f5fc8334b2f17605638751fcd1cf63d2 3921 bytes (raw)
name: extras/exec_cgi.rb 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
 
# -*- encoding: binary -*-
# Copyright (C) 2013-2018 all contributors <yahns-public@yhbt.net>
# License: GPL-2.0+ <https://www.gnu.org/licenses/gpl-2.0.txt>
# frozen_string_literal: true
#
# if running under yahns, worker_processes is recommended to avoid conflicting
# with the SIGCHLD handler in yahns.

# Be careful if using Rack::Deflater, this needs the following commit
# (currently in rack.git, not yet in 1.5.2):
#  commit 7bda8d485b38403bf07f43793d37b66b7a8281d6
#  (delfater: ensure that parent body is always closed)
# Otherwise you will get zombies from HEAD requests which accept compressed
# responses.
#
# Usage in config.ru using cgit as an example:
#
#   use Rack::Chunked
#   # other Rack middlewares can go here...
#
#   # cgit: https://git.zx2c4.com/cgit/
#   run ExecCgi.new('/path/to/cgit.cgi', opts)
#
class ExecCgi
  class MyIO
    attr_writer :my_pid
    attr_writer :body_tip
    attr_reader :rd

    def initialize(rd)
      @rd = rd
    end

    def each
      buf = @body_tip
      yield buf unless buf.empty?

      case tmp = @rd.read_nonblock(8192, buf, exception: false)
      when :wait_readable
        @rd.wait_readable
      when nil
        break
      else # String
        yield tmp
      end while true
      self
    ensure
      # do this sooner, since the response body may be buffered, we want
      # to release our FD as soon as possible.
      close
    end

    def close
      # yahns will call this again after its done writing the response
      # body, so we must ensure its idempotent.
      # Note: this object (and any client-specific objects) will never
      # be shared across different threads, so we do not need extra
      # mutual exclusion here.
      return if @rd.closed?
      @rd.close
      begin
        Process.waitpid(@my_pid)
      rescue Errno::ECHILD
      end if defined?(@my_pid) && @my_pid
    end
  end

  PASS_VARS = %w(
    CONTENT_LENGTH
    CONTENT_TYPE
    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
    SCRIPT_NAME
  ).map(&:freeze)  # frozen strings are faster for Hash assignments

  def initialize(*args)
    @env = Hash === args[0] ? args.shift : {}
    @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"
    @opts = Hash === args[-1] ? args.pop : {}
  end

  # Calls the app
  def call(env)
    env.delete('HTTP_PROXY') # ref: https://httpoxy.org/
    cgi_env = { "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_/ }

    rd, wr = IO.pipe
    io = MyIO.new(rd)
    errbody = io
    errbody.my_pid = spawn(cgi_env.merge!(@env), *@args,
                           @opts.merge(out: wr, close_others: true))
    wr.close

    begin
      head = rd.readpartial(8192)
      until head =~ /\r?\n\r?\n/
        tmp = rd.readpartial(8192)
        head << tmp
        tmp.clear
      end
      head, body = head.split(/\r?\n\r?\n/, 2)
      io.body_tip = body

      env["HTTP_VERSION"] ||= "HTTP/1.0" # stop Rack::Chunked for HTTP/0.9

      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
      errbody = nil
      [ status, headers, io ]
    rescue EOFError
      [ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
    end
  ensure
    errbody.close if errbody
  end
end

debug log:

solving 2746c09 ...
found 2746c09 in https://yhbt.net/yahns.git/

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this public inbox

	https://yhbt.net/yahns.git/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).