http: -Wshorten-64-to-32 warnings on clang
[unicorn.git] / lib / unicorn / app / exec_cgi.rb
blob232b6811888e8c31eee4097086862e1dd800b99a
1 # -*- encoding: binary -*-
2 # :enddoc:
3 require 'unicorn'
5 module Unicorn::App
7   # This class is highly experimental (even more so than the rest of Unicorn)
8   # and has never run anything other than cgit.
9   class ExecCgi < Struct.new(:args)
11     CHUNK_SIZE = 16384
12     PASS_VARS = %w(
13       CONTENT_LENGTH
14       CONTENT_TYPE
15       GATEWAY_INTERFACE
16       AUTH_TYPE
17       PATH_INFO
18       PATH_TRANSLATED
19       QUERY_STRING
20       REMOTE_ADDR
21       REMOTE_HOST
22       REMOTE_IDENT
23       REMOTE_USER
24       REQUEST_METHOD
25       SERVER_NAME
26       SERVER_PORT
27       SERVER_PROTOCOL
28       SERVER_SOFTWARE
29     ).map { |x| x.freeze } # frozen strings are faster for Hash assignments
31     class Body < Unicorn::TmpIO
32       def body_offset=(n)
33         sysseek(@body_offset = n)
34       end
36       def each
37         sysseek @body_offset
38         # don't use a preallocated buffer for sysread since we can't
39         # guarantee an actual socket is consuming the yielded string
40         # (or if somebody is pushing to an array for eventual concatenation
41         begin
42           yield sysread(CHUNK_SIZE)
43         rescue EOFError
44           break
45         end while true
46       end
47     end
49     # Intializes the app, example of usage in a config.ru
50     #   map "/cgit" do
51     #     run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
52     #   end
53     def initialize(*args)
54       self.args = args
55       first = args[0] or
56         raise ArgumentError, "need path to executable"
57       first[0] == ?/ or args[0] = ::File.expand_path(first)
58       File.executable?(args[0]) or
59         raise ArgumentError, "#{args[0]} is not executable"
60     end
62     # Calls the app
63     def call(env)
64       out, err = Body.new, Unicorn::TmpIO.new
65       inp = force_file_input(env)
66       pid = fork { run_child(inp, out, err, env) }
67       inp.close
68       pid, status = Process.waitpid2(pid)
69       write_errors(env, err, status) if err.stat.size > 0
70       err.close
72       return parse_output!(out) if status.success?
73       out.close
74       [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
75     end
77     private
79     def run_child(inp, out, err, env)
80       PASS_VARS.each do |key|
81         val = env[key] or next
82         ENV[key] = val
83       end
84       ENV['SCRIPT_NAME'] = args[0]
85       ENV['GATEWAY_INTERFACE'] = 'CGI/1.1'
86       env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] }
88       $stdin.reopen(inp)
89       $stdout.reopen(out)
90       $stderr.reopen(err)
91       exec(*args)
92     end
94     # Extracts headers from CGI out, will change the offset of out.
95     # This returns a standard Rack-compatible return value:
96     #   [ 200, HeadersHash, body ]
97     def parse_output!(out)
98       size = out.stat.size
99       out.sysseek(0)
100       head = out.sysread(CHUNK_SIZE)
101       offset = 2
102       head, body = head.split(/\n\n/, 2)
103       if body.nil?
104         head, body = head.split(/\r\n\r\n/, 2)
105         offset = 4
106       end
107       offset += head.length
108       out.body_offset = offset
109       size -= offset
110       prev = nil
111       headers = Rack::Utils::HeaderHash.new
112       head.split(/\r?\n/).each do |line|
113         case line
114         when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
115         when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
116         end
117       end
118       status = headers.delete("Status") || 200
119       headers['Content-Length'] = size.to_s
120       [ status, headers, out ]
121     end
123     # ensures rack.input is a file handle that we can redirect stdin to
124     def force_file_input(env)
125       inp = env['rack.input']
126       # inp could be a StringIO or StringIO-like object
127       if inp.respond_to?(:size) && inp.size == 0
128         ::File.open('/dev/null', 'rb')
129       else
130         tmp = Unicorn::TmpIO.new
132         buf = inp.read(CHUNK_SIZE)
133         begin
134           tmp.syswrite(buf)
135         end while inp.read(CHUNK_SIZE, buf)
136         tmp.sysseek(0)
137         tmp
138       end
139     end
141     # rack.errors this may not be an IO object, so we couldn't
142     # just redirect the CGI executable to that earlier.
143     def write_errors(env, err, status)
144       err.seek(0)
145       dst = env['rack.errors']
146       pid = status.pid
147       dst.write("#{pid}: #{args.inspect} status=#{status} stderr:\n")
148       err.each_line { |line| dst.write("#{pid}: #{line}") }
149       dst.flush
150     end
152   end