cgiapp.py 9.5 KB

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