cgiapp.py 9.0 KB

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