281 lines
9.5 KiB
Python
281 lines
9.5 KiB
Python
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
|
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
|
|
"""
|
|
Application that runs a CGI script.
|
|
"""
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
from six.moves.urllib.parse import quote
|
|
try:
|
|
import select
|
|
except ImportError:
|
|
select = None
|
|
import six
|
|
|
|
from paste.util import converters
|
|
|
|
__all__ = ['CGIError', 'CGIApplication']
|
|
|
|
class CGIError(Exception):
|
|
"""
|
|
Raised when the CGI script can't be found or doesn't
|
|
act like a proper CGI script.
|
|
"""
|
|
|
|
class CGIApplication(object):
|
|
|
|
"""
|
|
This object acts as a proxy to a CGI application. You pass in the
|
|
script path (``script``), an optional path to search for the
|
|
script (if the name isn't absolute) (``path``). If you don't give
|
|
a path, then ``$PATH`` will be used.
|
|
"""
|
|
|
|
def __init__(self,
|
|
global_conf,
|
|
script,
|
|
path=None,
|
|
include_os_environ=True,
|
|
query_string=None):
|
|
if global_conf:
|
|
raise NotImplemented(
|
|
"global_conf is no longer supported for CGIApplication "
|
|
"(use make_cgi_application); please pass None instead")
|
|
self.script_filename = script
|
|
if path is None:
|
|
path = os.environ.get('PATH', '').split(':')
|
|
self.path = path
|
|
if '?' in script:
|
|
assert query_string is None, (
|
|
"You cannot have '?' in your script name (%r) and also "
|
|
"give a query_string (%r)" % (script, query_string))
|
|
script, query_string = script.split('?', 1)
|
|
if os.path.abspath(script) != script:
|
|
# relative path
|
|
for path_dir in self.path:
|
|
if os.path.exists(os.path.join(path_dir, script)):
|
|
self.script = os.path.join(path_dir, script)
|
|
break
|
|
else:
|
|
raise CGIError(
|
|
"Script %r not found in path %r"
|
|
% (script, self.path))
|
|
else:
|
|
self.script = script
|
|
self.include_os_environ = include_os_environ
|
|
self.query_string = query_string
|
|
|
|
def __call__(self, environ, start_response):
|
|
if 'REQUEST_URI' not in environ:
|
|
environ['REQUEST_URI'] = (
|
|
quote(environ.get('SCRIPT_NAME', ''))
|
|
+ quote(environ.get('PATH_INFO', '')))
|
|
if self.include_os_environ:
|
|
cgi_environ = os.environ.copy()
|
|
else:
|
|
cgi_environ = {}
|
|
for name in environ:
|
|
# Should unicode values be encoded?
|
|
if (name.upper() == name
|
|
and isinstance(environ[name], str)):
|
|
cgi_environ[name] = environ[name]
|
|
if self.query_string is not None:
|
|
old = cgi_environ.get('QUERY_STRING', '')
|
|
if old:
|
|
old += '&'
|
|
cgi_environ['QUERY_STRING'] = old + self.query_string
|
|
cgi_environ['SCRIPT_FILENAME'] = self.script
|
|
proc = subprocess.Popen(
|
|
[self.script],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env=cgi_environ,
|
|
cwd=os.path.dirname(self.script),
|
|
)
|
|
writer = CGIWriter(environ, start_response)
|
|
if select and sys.platform != 'win32':
|
|
proc_communicate(
|
|
proc,
|
|
stdin=StdinReader.from_environ(environ),
|
|
stdout=writer,
|
|
stderr=environ['wsgi.errors'])
|
|
else:
|
|
stdout, stderr = proc.communicate(StdinReader.from_environ(environ).read())
|
|
if stderr:
|
|
environ['wsgi.errors'].write(stderr)
|
|
writer.write(stdout)
|
|
if not writer.headers_finished:
|
|
start_response(writer.status, writer.headers)
|
|
return []
|
|
|
|
class CGIWriter(object):
|
|
|
|
def __init__(self, environ, start_response):
|
|
self.environ = environ
|
|
self.start_response = start_response
|
|
self.status = '200 OK'
|
|
self.headers = []
|
|
self.headers_finished = False
|
|
self.writer = None
|
|
self.buffer = b''
|
|
|
|
def write(self, data):
|
|
if self.headers_finished:
|
|
self.writer(data)
|
|
return
|
|
self.buffer += data
|
|
while b'\n' in self.buffer:
|
|
if b'\r\n' in self.buffer and self.buffer.find(b'\r\n') < self.buffer.find(b'\n'):
|
|
line1, self.buffer = self.buffer.split(b'\r\n', 1)
|
|
else:
|
|
line1, self.buffer = self.buffer.split(b'\n', 1)
|
|
if not line1:
|
|
self.headers_finished = True
|
|
self.writer = self.start_response(
|
|
self.status, self.headers)
|
|
self.writer(self.buffer)
|
|
del self.buffer
|
|
del self.headers
|
|
del self.status
|
|
break
|
|
elif b':' not in line1:
|
|
raise CGIError(
|
|
"Bad header line: %r" % line1)
|
|
else:
|
|
name, value = line1.split(b':', 1)
|
|
value = value.lstrip()
|
|
name = name.strip()
|
|
if six.PY3:
|
|
name = name.decode('utf8')
|
|
value = value.decode('utf8')
|
|
if name.lower() == 'status':
|
|
if ' ' not in value:
|
|
# WSGI requires this space, sometimes CGI scripts don't set it:
|
|
value = '%s General' % value
|
|
self.status = value
|
|
else:
|
|
self.headers.append((name, value))
|
|
|
|
class StdinReader(object):
|
|
|
|
def __init__(self, stdin, content_length):
|
|
self.stdin = stdin
|
|
self.content_length = content_length
|
|
|
|
@classmethod
|
|
def from_environ(cls, environ):
|
|
length = environ.get('CONTENT_LENGTH')
|
|
if length:
|
|
length = int(length)
|
|
else:
|
|
length = 0
|
|
return cls(environ['wsgi.input'], length)
|
|
|
|
def read(self, size=None):
|
|
if not self.content_length:
|
|
return b''
|
|
if size is None:
|
|
text = self.stdin.read(self.content_length)
|
|
else:
|
|
text = self.stdin.read(min(self.content_length, size))
|
|
self.content_length -= len(text)
|
|
return text
|
|
|
|
def proc_communicate(proc, stdin=None, stdout=None, stderr=None):
|
|
"""
|
|
Run the given process, piping input/output/errors to the given
|
|
file-like objects (which need not be actual file objects, unlike
|
|
the arguments passed to Popen). Wait for process to terminate.
|
|
|
|
Note: this is taken from the posix version of
|
|
subprocess.Popen.communicate, but made more general through the
|
|
use of file-like objects.
|
|
"""
|
|
read_set = []
|
|
write_set = []
|
|
input_buffer = b''
|
|
trans_nl = proc.universal_newlines and hasattr(open, 'newlines')
|
|
|
|
if proc.stdin:
|
|
# Flush stdio buffer. This might block, if the user has
|
|
# been writing to .stdin in an uncontrolled fashion.
|
|
proc.stdin.flush()
|
|
if input:
|
|
write_set.append(proc.stdin)
|
|
else:
|
|
proc.stdin.close()
|
|
else:
|
|
assert stdin is None
|
|
if proc.stdout:
|
|
read_set.append(proc.stdout)
|
|
else:
|
|
assert stdout is None
|
|
if proc.stderr:
|
|
read_set.append(proc.stderr)
|
|
else:
|
|
assert stderr is None
|
|
|
|
while read_set or write_set:
|
|
rlist, wlist, xlist = select.select(read_set, write_set, [])
|
|
|
|
if proc.stdin in wlist:
|
|
# When select has indicated that the file is writable,
|
|
# we can write up to PIPE_BUF bytes without risk
|
|
# blocking. POSIX defines PIPE_BUF >= 512
|
|
next, input_buffer = input_buffer, b''
|
|
next_len = 512-len(next)
|
|
if next_len:
|
|
next += stdin.read(next_len)
|
|
if not next:
|
|
proc.stdin.close()
|
|
write_set.remove(proc.stdin)
|
|
else:
|
|
bytes_written = os.write(proc.stdin.fileno(), next)
|
|
if bytes_written < len(next):
|
|
input_buffer = next[bytes_written:]
|
|
|
|
if proc.stdout in rlist:
|
|
data = os.read(proc.stdout.fileno(), 1024)
|
|
if data == b"":
|
|
proc.stdout.close()
|
|
read_set.remove(proc.stdout)
|
|
if trans_nl:
|
|
data = proc._translate_newlines(data)
|
|
stdout.write(data)
|
|
|
|
if proc.stderr in rlist:
|
|
data = os.read(proc.stderr.fileno(), 1024)
|
|
if data == b"":
|
|
proc.stderr.close()
|
|
read_set.remove(proc.stderr)
|
|
if trans_nl:
|
|
data = proc._translate_newlines(data)
|
|
stderr.write(six.ensure_text(data))
|
|
|
|
try:
|
|
proc.wait()
|
|
except OSError as e:
|
|
if e.errno != 10:
|
|
raise
|
|
|
|
def make_cgi_application(global_conf, script, path=None, include_os_environ=None,
|
|
query_string=None):
|
|
"""
|
|
Paste Deploy interface for :class:`CGIApplication`
|
|
|
|
This object acts as a proxy to a CGI application. You pass in the
|
|
script path (``script``), an optional path to search for the
|
|
script (if the name isn't absolute) (``path``). If you don't give
|
|
a path, then ``$PATH`` will be used.
|
|
"""
|
|
if path is None:
|
|
path = global_conf.get('path') or global_conf.get('PATH')
|
|
include_os_environ = converters.asbool(include_os_environ)
|
|
return CGIApplication(
|
|
None,
|
|
script, path=path, include_os_environ=include_os_environ,
|
|
query_string=query_string)
|