/Lib/CGIHTTPServer.py

http://unladen-swallow.googlecode.com/ · Python · 366 lines · 299 code · 16 blank · 51 comment · 47 complexity · df6728d2511e83f2fdc2006a2b18c067 MD5 · raw file

  1. """CGI-savvy HTTP Server.
  2. This module builds on SimpleHTTPServer by implementing GET and POST
  3. requests to cgi-bin scripts.
  4. If the os.fork() function is not present (e.g. on Windows),
  5. os.popen2() is used as a fallback, with slightly altered semantics; if
  6. that function is not present either (e.g. on Macintosh), only Python
  7. scripts are supported, and they are executed by the current process.
  8. In all cases, the implementation is intentionally naive -- all
  9. requests are executed sychronously.
  10. SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
  11. -- it may execute arbitrary Python code or external programs.
  12. Note that status code 200 is sent prior to execution of a CGI script, so
  13. scripts cannot send other status codes such as 302 (redirect).
  14. """
  15. __version__ = "0.4"
  16. __all__ = ["CGIHTTPRequestHandler"]
  17. import os
  18. import sys
  19. import urllib
  20. import BaseHTTPServer
  21. import SimpleHTTPServer
  22. import select
  23. class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  24. """Complete HTTP server with GET, HEAD and POST commands.
  25. GET and HEAD also support running CGI scripts.
  26. The POST command is *only* implemented for CGI scripts.
  27. """
  28. # Determine platform specifics
  29. have_fork = hasattr(os, 'fork')
  30. have_popen2 = hasattr(os, 'popen2')
  31. have_popen3 = hasattr(os, 'popen3')
  32. # Make rfile unbuffered -- we need to read one line and then pass
  33. # the rest to a subprocess, so we can't use buffered input.
  34. rbufsize = 0
  35. def do_POST(self):
  36. """Serve a POST request.
  37. This is only implemented for CGI scripts.
  38. """
  39. if self.is_cgi():
  40. self.run_cgi()
  41. else:
  42. self.send_error(501, "Can only POST to CGI scripts")
  43. def send_head(self):
  44. """Version of send_head that support CGI scripts"""
  45. if self.is_cgi():
  46. return self.run_cgi()
  47. else:
  48. return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
  49. def is_cgi(self):
  50. """Test whether self.path corresponds to a CGI script,
  51. and return a boolean.
  52. This function sets self.cgi_info to a tuple (dir, rest)
  53. when it returns True, where dir is the directory part before
  54. the CGI script name. Note that rest begins with a
  55. slash if it is not empty.
  56. The default implementation tests whether the path
  57. begins with one of the strings in the list
  58. self.cgi_directories (and the next character is a '/'
  59. or the end of the string).
  60. """
  61. path = self.path
  62. for x in self.cgi_directories:
  63. i = len(x)
  64. if path[:i] == x and (not path[i:] or path[i] == '/'):
  65. self.cgi_info = path[:i], path[i+1:]
  66. return True
  67. return False
  68. cgi_directories = ['/cgi-bin', '/htbin']
  69. def is_executable(self, path):
  70. """Test whether argument path is an executable file."""
  71. return executable(path)
  72. def is_python(self, path):
  73. """Test whether argument path is a Python script."""
  74. head, tail = os.path.splitext(path)
  75. return tail.lower() in (".py", ".pyw")
  76. def run_cgi(self):
  77. """Execute a CGI script."""
  78. path = self.path
  79. dir, rest = self.cgi_info
  80. i = path.find('/', len(dir) + 1)
  81. while i >= 0:
  82. nextdir = path[:i]
  83. nextrest = path[i+1:]
  84. scriptdir = self.translate_path(nextdir)
  85. if os.path.isdir(scriptdir):
  86. dir, rest = nextdir, nextrest
  87. i = path.find('/', len(dir) + 1)
  88. else:
  89. break
  90. # find an explicit query string, if present.
  91. i = rest.rfind('?')
  92. if i >= 0:
  93. rest, query = rest[:i], rest[i+1:]
  94. else:
  95. query = ''
  96. # dissect the part after the directory name into a script name &
  97. # a possible additional path, to be stored in PATH_INFO.
  98. i = rest.find('/')
  99. if i >= 0:
  100. script, rest = rest[:i], rest[i:]
  101. else:
  102. script, rest = rest, ''
  103. scriptname = dir + '/' + script
  104. scriptfile = self.translate_path(scriptname)
  105. if not os.path.exists(scriptfile):
  106. self.send_error(404, "No such CGI script (%r)" % scriptname)
  107. return
  108. if not os.path.isfile(scriptfile):
  109. self.send_error(403, "CGI script is not a plain file (%r)" %
  110. scriptname)
  111. return
  112. ispy = self.is_python(scriptname)
  113. if not ispy:
  114. if not (self.have_fork or self.have_popen2 or self.have_popen3):
  115. self.send_error(403, "CGI script is not a Python script (%r)" %
  116. scriptname)
  117. return
  118. if not self.is_executable(scriptfile):
  119. self.send_error(403, "CGI script is not executable (%r)" %
  120. scriptname)
  121. return
  122. # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
  123. # XXX Much of the following could be prepared ahead of time!
  124. env = {}
  125. env['SERVER_SOFTWARE'] = self.version_string()
  126. env['SERVER_NAME'] = self.server.server_name
  127. env['GATEWAY_INTERFACE'] = 'CGI/1.1'
  128. env['SERVER_PROTOCOL'] = self.protocol_version
  129. env['SERVER_PORT'] = str(self.server.server_port)
  130. env['REQUEST_METHOD'] = self.command
  131. uqrest = urllib.unquote(rest)
  132. env['PATH_INFO'] = uqrest
  133. env['PATH_TRANSLATED'] = self.translate_path(uqrest)
  134. env['SCRIPT_NAME'] = scriptname
  135. if query:
  136. env['QUERY_STRING'] = query
  137. host = self.address_string()
  138. if host != self.client_address[0]:
  139. env['REMOTE_HOST'] = host
  140. env['REMOTE_ADDR'] = self.client_address[0]
  141. authorization = self.headers.getheader("authorization")
  142. if authorization:
  143. authorization = authorization.split()
  144. if len(authorization) == 2:
  145. import base64, binascii
  146. env['AUTH_TYPE'] = authorization[0]
  147. if authorization[0].lower() == "basic":
  148. try:
  149. authorization = base64.decodestring(authorization[1])
  150. except binascii.Error:
  151. pass
  152. else:
  153. authorization = authorization.split(':')
  154. if len(authorization) == 2:
  155. env['REMOTE_USER'] = authorization[0]
  156. # XXX REMOTE_IDENT
  157. if self.headers.typeheader is None:
  158. env['CONTENT_TYPE'] = self.headers.type
  159. else:
  160. env['CONTENT_TYPE'] = self.headers.typeheader
  161. length = self.headers.getheader('content-length')
  162. if length:
  163. env['CONTENT_LENGTH'] = length
  164. referer = self.headers.getheader('referer')
  165. if referer:
  166. env['HTTP_REFERER'] = referer
  167. accept = []
  168. for line in self.headers.getallmatchingheaders('accept'):
  169. if line[:1] in "\t\n\r ":
  170. accept.append(line.strip())
  171. else:
  172. accept = accept + line[7:].split(',')
  173. env['HTTP_ACCEPT'] = ','.join(accept)
  174. ua = self.headers.getheader('user-agent')
  175. if ua:
  176. env['HTTP_USER_AGENT'] = ua
  177. co = filter(None, self.headers.getheaders('cookie'))
  178. if co:
  179. env['HTTP_COOKIE'] = ', '.join(co)
  180. # XXX Other HTTP_* headers
  181. # Since we're setting the env in the parent, provide empty
  182. # values to override previously set values
  183. for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
  184. 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
  185. env.setdefault(k, "")
  186. os.environ.update(env)
  187. self.send_response(200, "Script output follows")
  188. decoded_query = query.replace('+', ' ')
  189. if self.have_fork:
  190. # Unix -- fork as we should
  191. args = [script]
  192. if '=' not in decoded_query:
  193. args.append(decoded_query)
  194. nobody = nobody_uid()
  195. self.wfile.flush() # Always flush before forking
  196. pid = os.fork()
  197. if pid != 0:
  198. # Parent
  199. pid, sts = os.waitpid(pid, 0)
  200. # throw away additional data [see bug #427345]
  201. while select.select([self.rfile], [], [], 0)[0]:
  202. if not self.rfile.read(1):
  203. break
  204. if sts:
  205. self.log_error("CGI script exit status %#x", sts)
  206. return
  207. # Child
  208. try:
  209. try:
  210. os.setuid(nobody)
  211. except os.error:
  212. pass
  213. os.dup2(self.rfile.fileno(), 0)
  214. os.dup2(self.wfile.fileno(), 1)
  215. os.execve(scriptfile, args, os.environ)
  216. except:
  217. self.server.handle_error(self.request, self.client_address)
  218. os._exit(127)
  219. elif self.have_popen2 or self.have_popen3:
  220. # Windows -- use popen2 or popen3 to create a subprocess
  221. import shutil
  222. if self.have_popen3:
  223. popenx = os.popen3
  224. else:
  225. popenx = os.popen2
  226. cmdline = scriptfile
  227. if self.is_python(scriptfile):
  228. interp = sys.executable
  229. if interp.lower().endswith("w.exe"):
  230. # On Windows, use python.exe, not pythonw.exe
  231. interp = interp[:-5] + interp[-4:]
  232. cmdline = "%s -u %s" % (interp, cmdline)
  233. if '=' not in query and '"' not in query:
  234. cmdline = '%s "%s"' % (cmdline, query)
  235. self.log_message("command: %s", cmdline)
  236. try:
  237. nbytes = int(length)
  238. except (TypeError, ValueError):
  239. nbytes = 0
  240. files = popenx(cmdline, 'b')
  241. fi = files[0]
  242. fo = files[1]
  243. if self.have_popen3:
  244. fe = files[2]
  245. if self.command.lower() == "post" and nbytes > 0:
  246. data = self.rfile.read(nbytes)
  247. fi.write(data)
  248. # throw away additional data [see bug #427345]
  249. while select.select([self.rfile._sock], [], [], 0)[0]:
  250. if not self.rfile._sock.recv(1):
  251. break
  252. fi.close()
  253. shutil.copyfileobj(fo, self.wfile)
  254. if self.have_popen3:
  255. errors = fe.read()
  256. fe.close()
  257. if errors:
  258. self.log_error('%s', errors)
  259. sts = fo.close()
  260. if sts:
  261. self.log_error("CGI script exit status %#x", sts)
  262. else:
  263. self.log_message("CGI script exited OK")
  264. else:
  265. # Other O.S. -- execute script in this process
  266. save_argv = sys.argv
  267. save_stdin = sys.stdin
  268. save_stdout = sys.stdout
  269. save_stderr = sys.stderr
  270. try:
  271. save_cwd = os.getcwd()
  272. try:
  273. sys.argv = [scriptfile]
  274. if '=' not in decoded_query:
  275. sys.argv.append(decoded_query)
  276. sys.stdout = self.wfile
  277. sys.stdin = self.rfile
  278. execfile(scriptfile, {"__name__": "__main__"})
  279. finally:
  280. sys.argv = save_argv
  281. sys.stdin = save_stdin
  282. sys.stdout = save_stdout
  283. sys.stderr = save_stderr
  284. os.chdir(save_cwd)
  285. except SystemExit, sts:
  286. self.log_error("CGI script exit status %s", str(sts))
  287. else:
  288. self.log_message("CGI script exited OK")
  289. nobody = None
  290. def nobody_uid():
  291. """Internal routine to get nobody's uid"""
  292. global nobody
  293. if nobody:
  294. return nobody
  295. try:
  296. import pwd
  297. except ImportError:
  298. return -1
  299. try:
  300. nobody = pwd.getpwnam('nobody')[2]
  301. except KeyError:
  302. nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
  303. return nobody
  304. def executable(path):
  305. """Test for executable file."""
  306. try:
  307. st = os.stat(path)
  308. except os.error:
  309. return False
  310. return st.st_mode & 0111 != 0
  311. def test(HandlerClass = CGIHTTPRequestHandler,
  312. ServerClass = BaseHTTPServer.HTTPServer):
  313. SimpleHTTPServer.test(HandlerClass, ServerClass)
  314. if __name__ == '__main__':
  315. test()