1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
---|
2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
---|
3 | |
---|
4 | """ |
---|
5 | Application that runs a CGI script. |
---|
6 | """ |
---|
7 | import os |
---|
8 | import subprocess |
---|
9 | try: |
---|
10 | import select |
---|
11 | except ImportError: |
---|
12 | select = None |
---|
13 | |
---|
14 | from paste.util import converters |
---|
15 | |
---|
16 | __all__ = ['CGIError', 'CGIApplication'] |
---|
17 | |
---|
18 | class CGIError(Exception): |
---|
19 | """ |
---|
20 | Raised when the CGI script can't be found or doesn't |
---|
21 | act like a proper CGI script. |
---|
22 | """ |
---|
23 | |
---|
24 | class CGIApplication(object): |
---|
25 | |
---|
26 | """ |
---|
27 | This object acts as a proxy to a CGI application. You pass in the |
---|
28 | script path (``script``), an optional path to search for the |
---|
29 | script (if the name isn't absolute) (``path``). If you don't give |
---|
30 | a path, then ``$PATH`` will be used. |
---|
31 | """ |
---|
32 | |
---|
33 | def __init__(self, |
---|
34 | global_conf, |
---|
35 | script, |
---|
36 | path=None, |
---|
37 | include_os_environ=True, |
---|
38 | query_string=None): |
---|
39 | if global_conf: |
---|
40 | raise NotImplemented( |
---|
41 | "global_conf is no longer supported for CGIApplication " |
---|
42 | "(use make_cgi_application); please pass None instead") |
---|
43 | self.script_filename = script |
---|
44 | if path is None: |
---|
45 | path = os.environ.get('PATH', '').split(':') |
---|
46 | self.path = path |
---|
47 | if '?' in script: |
---|
48 | assert query_string is None, ( |
---|
49 | "You cannot have '?' in your script name (%r) and also " |
---|
50 | "give a query_string (%r)" % (script, query_string)) |
---|
51 | script, query_string = script.split('?', 1) |
---|
52 | if os.path.abspath(script) != script: |
---|
53 | # relative path |
---|
54 | for path_dir in self.path: |
---|
55 | if os.path.exists(os.path.join(path_dir, script)): |
---|
56 | self.script = os.path.join(path_dir, script) |
---|
57 | break |
---|
58 | else: |
---|
59 | raise CGIError( |
---|
60 | "Script %r not found in path %r" |
---|
61 | % (script, self.path)) |
---|
62 | else: |
---|
63 | self.script = script |
---|
64 | self.include_os_environ = include_os_environ |
---|
65 | self.query_string = query_string |
---|
66 | |
---|
67 | def __call__(self, environ, start_response): |
---|
68 | if 'REQUEST_URI' not in environ: |
---|
69 | environ['REQUEST_URI'] = ( |
---|
70 | environ.get('SCRIPT_NAME', '') |
---|
71 | + environ.get('PATH_INFO', '')) |
---|
72 | if self.include_os_environ: |
---|
73 | cgi_environ = os.environ.copy() |
---|
74 | else: |
---|
75 | cgi_environ = {} |
---|
76 | for name in environ: |
---|
77 | # Should unicode values be encoded? |
---|
78 | if (name.upper() == name |
---|
79 | and isinstance(environ[name], str)): |
---|
80 | cgi_environ[name] = environ[name] |
---|
81 | if self.query_string is not None: |
---|
82 | old = cgi_environ.get('QUERY_STRING', '') |
---|
83 | if old: |
---|
84 | old += '&' |
---|
85 | cgi_environ['QUERY_STRING'] = old + self.query_string |
---|
86 | cgi_environ['SCRIPT_FILENAME'] = self.script |
---|
87 | proc = subprocess.Popen( |
---|
88 | [self.script], |
---|
89 | stdin=subprocess.PIPE, |
---|
90 | stdout=subprocess.PIPE, |
---|
91 | stderr=subprocess.PIPE, |
---|
92 | env=cgi_environ, |
---|
93 | cwd=os.path.dirname(self.script), |
---|
94 | ) |
---|
95 | writer = CGIWriter(environ, start_response) |
---|
96 | if select: |
---|
97 | proc_communicate( |
---|
98 | proc, |
---|
99 | stdin=StdinReader.from_environ(environ), |
---|
100 | stdout=writer, |
---|
101 | stderr=environ['wsgi.errors']) |
---|
102 | else: |
---|
103 | stdout, stderr = proc.communicate(StdinReader.from_environ(environ).read()) |
---|
104 | if stderr: |
---|
105 | environ['wsgi.errors'].write(stderr) |
---|
106 | writer(stdout) |
---|
107 | if not writer.headers_finished: |
---|
108 | start_response(writer.status, writer.headers) |
---|
109 | return [] |
---|
110 | |
---|
111 | class CGIWriter(object): |
---|
112 | |
---|
113 | def __init__(self, environ, start_response): |
---|
114 | self.environ = environ |
---|
115 | self.start_response = start_response |
---|
116 | self.status = '200 OK' |
---|
117 | self.headers = [] |
---|
118 | self.headers_finished = False |
---|
119 | self.writer = None |
---|
120 | self.buffer = '' |
---|
121 | |
---|
122 | def write(self, data): |
---|
123 | if self.headers_finished: |
---|
124 | self.writer(data) |
---|
125 | return |
---|
126 | self.buffer += data |
---|
127 | while '\n' in self.buffer: |
---|
128 | if '\r\n' in self.buffer: |
---|
129 | line1, self.buffer = self.buffer.split('\r\n', 1) |
---|
130 | else: |
---|
131 | line1, self.buffer = self.buffer.split('\n', 1) |
---|
132 | if not line1: |
---|
133 | self.headers_finished = True |
---|
134 | self.writer = self.start_response( |
---|
135 | self.status, self.headers) |
---|
136 | self.writer(self.buffer) |
---|
137 | del self.buffer |
---|
138 | del self.headers |
---|
139 | del self.status |
---|
140 | break |
---|
141 | elif ':' not in line1: |
---|
142 | raise CGIError( |
---|
143 | "Bad header line: %r" % line1) |
---|
144 | else: |
---|
145 | name, value = line1.split(':', 1) |
---|
146 | value = value.lstrip() |
---|
147 | name = name.strip() |
---|
148 | if name.lower() == 'status': |
---|
149 | self.status = value |
---|
150 | else: |
---|
151 | self.headers.append((name, value)) |
---|
152 | |
---|
153 | class StdinReader(object): |
---|
154 | |
---|
155 | def __init__(self, stdin, content_length): |
---|
156 | self.stdin = stdin |
---|
157 | self.content_length = content_length |
---|
158 | |
---|
159 | def from_environ(cls, environ): |
---|
160 | length = environ.get('CONTENT_LENGTH') |
---|
161 | if length: |
---|
162 | length = int(length) |
---|
163 | else: |
---|
164 | length = 0 |
---|
165 | return cls(environ['wsgi.input'], length) |
---|
166 | |
---|
167 | from_environ = classmethod(from_environ) |
---|
168 | |
---|
169 | def read(self, size=None): |
---|
170 | if not self.content_length: |
---|
171 | return '' |
---|
172 | if size is None: |
---|
173 | text = self.stdin.read(self.content_length) |
---|
174 | else: |
---|
175 | text = self.stdin.read(min(self.content_length, size)) |
---|
176 | self.content_length -= len(text) |
---|
177 | return text |
---|
178 | |
---|
179 | def proc_communicate(proc, stdin=None, stdout=None, stderr=None): |
---|
180 | """ |
---|
181 | Run the given process, piping input/output/errors to the given |
---|
182 | file-like objects (which need not be actual file objects, unlike |
---|
183 | the arguments passed to Popen). Wait for process to terminate. |
---|
184 | |
---|
185 | Note: this is taken from the posix version of |
---|
186 | subprocess.Popen.communicate, but made more general through the |
---|
187 | use of file-like objects. |
---|
188 | """ |
---|
189 | read_set = [] |
---|
190 | write_set = [] |
---|
191 | input_buffer = '' |
---|
192 | trans_nl = proc.universal_newlines and hasattr(open, 'newlines') |
---|
193 | |
---|
194 | if proc.stdin: |
---|
195 | # Flush stdio buffer. This might block, if the user has |
---|
196 | # been writing to .stdin in an uncontrolled fashion. |
---|
197 | proc.stdin.flush() |
---|
198 | if input: |
---|
199 | write_set.append(proc.stdin) |
---|
200 | else: |
---|
201 | proc.stdin.close() |
---|
202 | else: |
---|
203 | assert stdin is None |
---|
204 | if proc.stdout: |
---|
205 | read_set.append(proc.stdout) |
---|
206 | else: |
---|
207 | assert stdout is None |
---|
208 | if proc.stderr: |
---|
209 | read_set.append(proc.stderr) |
---|
210 | else: |
---|
211 | assert stderr is None |
---|
212 | |
---|
213 | while read_set or write_set: |
---|
214 | rlist, wlist, xlist = select.select(read_set, write_set, []) |
---|
215 | |
---|
216 | if proc.stdin in wlist: |
---|
217 | # When select has indicated that the file is writable, |
---|
218 | # we can write up to PIPE_BUF bytes without risk |
---|
219 | # blocking. POSIX defines PIPE_BUF >= 512 |
---|
220 | next, input_buffer = input_buffer, '' |
---|
221 | next_len = 512-len(next) |
---|
222 | if next_len: |
---|
223 | next += stdin.read(next_len) |
---|
224 | if not next: |
---|
225 | proc.stdin.close() |
---|
226 | write_set.remove(proc.stdin) |
---|
227 | else: |
---|
228 | bytes_written = os.write(proc.stdin.fileno(), next) |
---|
229 | if bytes_written < len(next): |
---|
230 | input_buffer = next[bytes_written:] |
---|
231 | |
---|
232 | if proc.stdout in rlist: |
---|
233 | data = os.read(proc.stdout.fileno(), 1024) |
---|
234 | if data == "": |
---|
235 | proc.stdout.close() |
---|
236 | read_set.remove(proc.stdout) |
---|
237 | if trans_nl: |
---|
238 | data = proc._translate_newlines(data) |
---|
239 | stdout.write(data) |
---|
240 | |
---|
241 | if proc.stderr in rlist: |
---|
242 | data = os.read(proc.stderr.fileno(), 1024) |
---|
243 | if data == "": |
---|
244 | proc.stderr.close() |
---|
245 | read_set.remove(proc.stderr) |
---|
246 | if trans_nl: |
---|
247 | data = proc._translate_newlines(data) |
---|
248 | stderr.write(data) |
---|
249 | |
---|
250 | try: |
---|
251 | proc.wait() |
---|
252 | except OSError, e: |
---|
253 | if e.errno != 10: |
---|
254 | raise |
---|
255 | |
---|
256 | def make_cgi_application(global_conf, script, path=None, include_os_environ=None, |
---|
257 | query_string=None): |
---|
258 | """ |
---|
259 | This object acts as a proxy to a CGI application. You pass in the |
---|
260 | script path (``script``), an optional path to search for the |
---|
261 | script (if the name isn't absolute) (``path``). If you don't give |
---|
262 | a path, then ``$PATH`` will be used. |
---|
263 | """ |
---|
264 | if path is None: |
---|
265 | path = global_conf.get('path') or global_conf.get('PATH') |
---|
266 | include_os_environ = converters.asbool(include_os_environ) |
---|
267 | return CGIApplication( |
---|
268 | script, path=path, include_os_environ=include_os_environ, |
---|
269 | query_string=query_string) |
---|