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