0001 """CGI-savvy HTTP Server. 0002 0003 This module builds on SimpleHTTPServer by implementing GET and POST 0004 requests to cgi-bin scripts. 0005 0006 If the os.fork() function is not present (e.g. on Windows), 0007 os.popen2() is used as a fallback, with slightly altered semantics; if 0008 that function is not present either (e.g. on Macintosh), only Python 0009 scripts are supported, and they are executed by the current process. 0010 0011 In all cases, the implementation is intentionally naive -- all 0012 requests are executed sychronously. 0013 0014 SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL 0015 -- it may execute arbitrary Python code or external programs. 0016 0017 """ 0018 0019 0020 __version__ = "0.4" 0021 0022 __all__ = ["CGIHTTPRequestHandler"] 0023 0024 import os 0025 import sys 0026 import urllib 0027 import BaseHTTPServer 0028 import SimpleHTTPServer 0029 import select 0030 0031 0032 class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 0033 0034 """Complete HTTP server with GET, HEAD and POST commands. 0035 0036 GET and HEAD also support running CGI scripts. 0037 0038 The POST command is *only* implemented for CGI scripts. 0039 0040 """ 0041 0042 # Determine platform specifics 0043 have_fork = hasattr(os, 'fork') 0044 have_popen2 = hasattr(os, 'popen2') 0045 have_popen3 = hasattr(os, 'popen3') 0046 0047 # Make rfile unbuffered -- we need to read one line and then pass 0048 # the rest to a subprocess, so we can't use buffered input. 0049 rbufsize = 0 0050 0051 def do_POST(self): 0052 """Serve a POST request. 0053 0054 This is only implemented for CGI scripts. 0055 0056 """ 0057 0058 if self.is_cgi(): 0059 self.run_cgi() 0060 else: 0061 self.send_error(501, "Can only POST to CGI scripts") 0062 0063 def send_head(self): 0064 """Version of send_head that support CGI scripts""" 0065 if self.is_cgi(): 0066 return self.run_cgi() 0067 else: 0068 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) 0069 0070 def is_cgi(self): 0071 """Test whether self.path corresponds to a CGI script. 0072 0073 Return a tuple (dir, rest) if self.path requires running a 0074 CGI script, None if not. Note that rest begins with a 0075 slash if it is not empty. 0076 0077 The default implementation tests whether the path 0078 begins with one of the strings in the list 0079 self.cgi_directories (and the next character is a '/' 0080 or the end of the string). 0081 0082 """ 0083 0084 path = self.path 0085 0086 for x in self.cgi_directories: 0087 i = len(x) 0088 if path[:i] == x and (not path[i:] or path[i] == '/'): 0089 self.cgi_info = path[:i], path[i+1:] 0090 return True 0091 return False 0092 0093 cgi_directories = ['/cgi-bin', '/htbin'] 0094 0095 def is_executable(self, path): 0096 """Test whether argument path is an executable file.""" 0097 return executable(path) 0098 0099 def is_python(self, path): 0100 """Test whether argument path is a Python script.""" 0101 head, tail = os.path.splitext(path) 0102 return tail.lower() in (".py", ".pyw") 0103 0104 def run_cgi(self): 0105 """Execute a CGI script.""" 0106 dir, rest = self.cgi_info 0107 i = rest.rfind('?') 0108 if i >= 0: 0109 rest, query = rest[:i], rest[i+1:] 0110 else: 0111 query = '' 0112 i = rest.find('/') 0113 if i >= 0: 0114 script, rest = rest[:i], rest[i:] 0115 else: 0116 script, rest = rest, '' 0117 scriptname = dir + '/' + script 0118 scriptfile = self.translate_path(scriptname) 0119 if not os.path.exists(scriptfile): 0120 self.send_error(404, "No such CGI script (%r)" % scriptname) 0121 return 0122 if not os.path.isfile(scriptfile): 0123 self.send_error(403, "CGI script is not a plain file (%r)" % 0124 scriptname) 0125 return 0126 ispy = self.is_python(scriptname) 0127 if not ispy: 0128 if not (self.have_fork or self.have_popen2 or self.have_popen3): 0129 self.send_error(403, "CGI script is not a Python script (%r)" % 0130 scriptname) 0131 return 0132 if not self.is_executable(scriptfile): 0133 self.send_error(403, "CGI script is not executable (%r)" % 0134 scriptname) 0135 return 0136 0137 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html 0138 # XXX Much of the following could be prepared ahead of time! 0139 env = {} 0140 env['SERVER_SOFTWARE'] = self.version_string() 0141 env['SERVER_NAME'] = self.server.server_name 0142 env['GATEWAY_INTERFACE'] = 'CGI/1.1' 0143 env['SERVER_PROTOCOL'] = self.protocol_version 0144 env['SERVER_PORT'] = str(self.server.server_port) 0145 env['REQUEST_METHOD'] = self.command 0146 uqrest = urllib.unquote(rest) 0147 env['PATH_INFO'] = uqrest 0148 env['PATH_TRANSLATED'] = self.translate_path(uqrest) 0149 env['SCRIPT_NAME'] = scriptname 0150 if query: 0151 env['QUERY_STRING'] = query 0152 host = self.address_string() 0153 if host != self.client_address[0]: 0154 env['REMOTE_HOST'] = host 0155 env['REMOTE_ADDR'] = self.client_address[0] 0156 authorization = self.headers.getheader("authorization") 0157 if authorization: 0158 authorization = authorization.split() 0159 if len(authorization) == 2: 0160 import base64, binascii 0161 env['AUTH_TYPE'] = authorization[0] 0162 if authorization[0].lower() == "basic": 0163 try: 0164 authorization = base64.decodestring(authorization[1]) 0165 except binascii.Error: 0166 pass 0167 else: 0168 authorization = authorization.split(':') 0169 if len(authorization) == 2: 0170 env['REMOTE_USER'] = authorization[0] 0171 # XXX REMOTE_IDENT 0172 if self.headers.typeheader is None: 0173 env['CONTENT_TYPE'] = self.headers.type 0174 else: 0175 env['CONTENT_TYPE'] = self.headers.typeheader 0176 length = self.headers.getheader('content-length') 0177 if length: 0178 env['CONTENT_LENGTH'] = length 0179 accept = [] 0180 for line in self.headers.getallmatchingheaders('accept'): 0181 if line[:1] in "\t\n\r ": 0182 accept.append(line.strip()) 0183 else: 0184 accept = accept + line[7:].split(',') 0185 env['HTTP_ACCEPT'] = ','.join(accept) 0186 ua = self.headers.getheader('user-agent') 0187 if ua: 0188 env['HTTP_USER_AGENT'] = ua 0189 co = filter(None, self.headers.getheaders('cookie')) 0190 if co: 0191 env['HTTP_COOKIE'] = ', '.join(co) 0192 # XXX Other HTTP_* headers 0193 # Since we're setting the env in the parent, provide empty 0194 # values to override previously set values 0195 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', 0196 'HTTP_USER_AGENT', 'HTTP_COOKIE'): 0197 env.setdefault(k, "") 0198 os.environ.update(env) 0199 0200 self.send_response(200, "Script output follows") 0201 0202 decoded_query = query.replace('+', ' ') 0203 0204 if self.have_fork: 0205 # Unix -- fork as we should 0206 args = [script] 0207 if '=' not in decoded_query: 0208 args.append(decoded_query) 0209 nobody = nobody_uid() 0210 self.wfile.flush() # Always flush before forking 0211 pid = os.fork() 0212 if pid != 0: 0213 # Parent 0214 pid, sts = os.waitpid(pid, 0) 0215 # throw away additional data [see bug #427345] 0216 while select.select([self.rfile], [], [], 0)[0]: 0217 if not self.rfile.read(1): 0218 break 0219 if sts: 0220 self.log_error("CGI script exit status %#x", sts) 0221 return 0222 # Child 0223 try: 0224 try: 0225 os.setuid(nobody) 0226 except os.error: 0227 pass 0228 os.dup2(self.rfile.fileno(), 0) 0229 os.dup2(self.wfile.fileno(), 1) 0230 os.execve(scriptfile, args, os.environ) 0231 except: 0232 self.server.handle_error(self.request, self.client_address) 0233 os._exit(127) 0234 0235 elif self.have_popen2 or self.have_popen3: 0236 # Windows -- use popen2 or popen3 to create a subprocess 0237 import shutil 0238 if self.have_popen3: 0239 popenx = os.popen3 0240 else: 0241 popenx = os.popen2 0242 cmdline = scriptfile 0243 if self.is_python(scriptfile): 0244 interp = sys.executable 0245 if interp.lower().endswith("w.exe"): 0246 # On Windows, use python.exe, not pythonw.exe 0247 interp = interp[:-5] + interp[-4:] 0248 cmdline = "%s -u %s" % (interp, cmdline) 0249 if '=' not in query and '"' not in query: 0250 cmdline = '%s "%s"' % (cmdline, query) 0251 self.log_message("command: %s", cmdline) 0252 try: 0253 nbytes = int(length) 0254 except (TypeError, ValueError): 0255 nbytes = 0 0256 files = popenx(cmdline, 'b') 0257 fi = files[0] 0258 fo = files[1] 0259 if self.have_popen3: 0260 fe = files[2] 0261 if self.command.lower() == "post" and nbytes > 0: 0262 data = self.rfile.read(nbytes) 0263 fi.write(data) 0264 # throw away additional data [see bug #427345] 0265 while select.select([self.rfile._sock], [], [], 0)[0]: 0266 if not self.rfile._sock.recv(1): 0267 break 0268 fi.close() 0269 shutil.copyfileobj(fo, self.wfile) 0270 if self.have_popen3: 0271 errors = fe.read() 0272 fe.close() 0273 if errors: 0274 self.log_error('%s', errors) 0275 sts = fo.close() 0276 if sts: 0277 self.log_error("CGI script exit status %#x", sts) 0278 else: 0279 self.log_message("CGI script exited OK") 0280 0281 else: 0282 # Other O.S. -- execute script in this process 0283 save_argv = sys.argv 0284 save_stdin = sys.stdin 0285 save_stdout = sys.stdout 0286 save_stderr = sys.stderr 0287 try: 0288 save_cwd = os.getcwd() 0289 try: 0290 sys.argv = [scriptfile] 0291 if '=' not in decoded_query: 0292 sys.argv.append(decoded_query) 0293 sys.stdout = self.wfile 0294 sys.stdin = self.rfile 0295 execfile(scriptfile, {"__name__": "__main__"}) 0296 finally: 0297 sys.argv = save_argv 0298 sys.stdin = save_stdin 0299 sys.stdout = save_stdout 0300 sys.stderr = save_stderr 0301 os.chdir(save_cwd) 0302 except SystemExit, sts: 0303 self.log_error("CGI script exit status %s", str(sts)) 0304 else: 0305 self.log_message("CGI script exited OK") 0306 0307 0308 nobody = None 0309 0310 def nobody_uid(): 0311 """Internal routine to get nobody's uid""" 0312 global nobody 0313 if nobody: 0314 return nobody 0315 try: 0316 import pwd 0317 except ImportError: 0318 return -1 0319 try: 0320 nobody = pwd.getpwnam('nobody')[2] 0321 except KeyError: 0322 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall())) 0323 return nobody 0324 0325 0326 def executable(path): 0327 """Test for executable file.""" 0328 try: 0329 st = os.stat(path) 0330 except os.error: 0331 return False 0332 return st.st_mode & 0111 != 0 0333 0334 0335 def test(HandlerClass = CGIHTTPRequestHandler, 0336 ServerClass = BaseHTTPServer.HTTPServer): 0337 SimpleHTTPServer.test(HandlerClass, ServerClass) 0338 0339 0340 if __name__ == '__main__': 0341 test() 0342
Generated by PyXR 0.9.4