CGIHTTPServer.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. import copy
  24. class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  25. """Complete HTTP server with GET, HEAD and POST commands.
  26. GET and HEAD also support running CGI scripts.
  27. The POST command is *only* implemented for CGI scripts.
  28. """
  29. # Determine platform specifics
  30. have_fork = hasattr(os, 'fork')
  31. have_popen2 = hasattr(os, 'popen2')
  32. have_popen3 = hasattr(os, 'popen3')
  33. # Make rfile unbuffered -- we need to read one line and then pass
  34. # the rest to a subprocess, so we can't use buffered input.
  35. rbufsize = 0
  36. def do_POST(self):
  37. """Serve a POST request.
  38. This is only implemented for CGI scripts.
  39. """
  40. if self.is_cgi():
  41. self.run_cgi()
  42. else:
  43. self.send_error(501, "Can only POST to CGI scripts")
  44. def send_head(self):
  45. """Version of send_head that support CGI scripts"""
  46. if self.is_cgi():
  47. return self.run_cgi()
  48. else:
  49. return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
  50. def is_cgi(self):
  51. """Test whether self.path corresponds to a CGI script.
  52. Returns True and updates the cgi_info attribute to the tuple
  53. (dir, rest) if self.path requires running a CGI script.
  54. Returns False otherwise.
  55. If any exception is raised, the caller should assume that
  56. self.path was rejected as invalid and act accordingly.
  57. The default implementation tests whether the normalized url
  58. path begins with one of the strings in self.cgi_directories
  59. (and the next character is a '/' or the end of the string).
  60. """
  61. collapsed_path = _url_collapse_path(self.path)
  62. dir_sep = collapsed_path.find('/', 1)
  63. head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
  64. if head in self.cgi_directories:
  65. self.cgi_info = head, tail
  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. dir, rest = self.cgi_info
  79. path = dir + '/' + rest
  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. rest, _, query = rest.partition('?')
  92. # dissect the part after the directory name into a script name &
  93. # a possible additional path, to be stored in PATH_INFO.
  94. i = rest.find('/')
  95. if i >= 0:
  96. script, rest = rest[:i], rest[i:]
  97. else:
  98. script, rest = rest, ''
  99. scriptname = dir + '/' + script
  100. scriptfile = self.translate_path(scriptname)
  101. if not os.path.exists(scriptfile):
  102. self.send_error(404, "No such CGI script (%r)" % scriptname)
  103. return
  104. if not os.path.isfile(scriptfile):
  105. self.send_error(403, "CGI script is not a plain file (%r)" %
  106. scriptname)
  107. return
  108. ispy = self.is_python(scriptname)
  109. if not ispy:
  110. if not (self.have_fork or self.have_popen2 or self.have_popen3):
  111. self.send_error(403, "CGI script is not a Python script (%r)" %
  112. scriptname)
  113. return
  114. if not self.is_executable(scriptfile):
  115. self.send_error(403, "CGI script is not executable (%r)" %
  116. scriptname)
  117. return
  118. # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
  119. # XXX Much of the following could be prepared ahead of time!
  120. env = copy.deepcopy(os.environ)
  121. env['SERVER_SOFTWARE'] = self.version_string()
  122. env['SERVER_NAME'] = self.server.server_name
  123. env['GATEWAY_INTERFACE'] = 'CGI/1.1'
  124. env['SERVER_PROTOCOL'] = self.protocol_version
  125. env['SERVER_PORT'] = str(self.server.server_port)
  126. env['REQUEST_METHOD'] = self.command
  127. uqrest = urllib.unquote(rest)
  128. env['PATH_INFO'] = uqrest
  129. env['PATH_TRANSLATED'] = self.translate_path(uqrest)
  130. env['SCRIPT_NAME'] = scriptname
  131. if query:
  132. env['QUERY_STRING'] = query
  133. host = self.address_string()
  134. if host != self.client_address[0]:
  135. env['REMOTE_HOST'] = host
  136. env['REMOTE_ADDR'] = self.client_address[0]
  137. authorization = self.headers.getheader("authorization")
  138. if authorization:
  139. authorization = authorization.split()
  140. if len(authorization) == 2:
  141. import base64, binascii
  142. env['AUTH_TYPE'] = authorization[0]
  143. if authorization[0].lower() == "basic":
  144. try:
  145. authorization = base64.decodestring(authorization[1])
  146. except binascii.Error:
  147. pass
  148. else:
  149. authorization = authorization.split(':')
  150. if len(authorization) == 2:
  151. env['REMOTE_USER'] = authorization[0]
  152. # XXX REMOTE_IDENT
  153. if self.headers.typeheader is None:
  154. env['CONTENT_TYPE'] = self.headers.type
  155. else:
  156. env['CONTENT_TYPE'] = self.headers.typeheader
  157. length = self.headers.getheader('content-length')
  158. if length:
  159. env['CONTENT_LENGTH'] = length
  160. referer = self.headers.getheader('referer')
  161. if referer:
  162. env['HTTP_REFERER'] = referer
  163. accept = []
  164. for line in self.headers.getallmatchingheaders('accept'):
  165. if line[:1] in "\t\n\r ":
  166. accept.append(line.strip())
  167. else:
  168. accept = accept + line[7:].split(',')
  169. env['HTTP_ACCEPT'] = ','.join(accept)
  170. ua = self.headers.getheader('user-agent')
  171. if ua:
  172. env['HTTP_USER_AGENT'] = ua
  173. co = filter(None, self.headers.getheaders('cookie'))
  174. if co:
  175. env['HTTP_COOKIE'] = ', '.join(co)
  176. # XXX Other HTTP_* headers
  177. # Since we're setting the env in the parent, provide empty
  178. # values to override previously set values
  179. for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
  180. 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
  181. env.setdefault(k, "")
  182. self.send_response(200, "Script output follows")
  183. decoded_query = query.replace('+', ' ')
  184. if self.have_fork:
  185. # Unix -- fork as we should
  186. args = [script]
  187. if '=' not in decoded_query:
  188. args.append(decoded_query)
  189. nobody = nobody_uid()
  190. self.wfile.flush() # Always flush before forking
  191. pid = os.fork()
  192. if pid != 0:
  193. # Parent
  194. pid, sts = os.waitpid(pid, 0)
  195. # throw away additional data [see bug #427345]
  196. while select.select([self.rfile], [], [], 0)[0]:
  197. if not self.rfile.read(1):
  198. break
  199. if sts:
  200. self.log_error("CGI script exit status %#x", sts)
  201. return
  202. # Child
  203. try:
  204. try:
  205. os.setuid(nobody)
  206. except os.error:
  207. pass
  208. os.dup2(self.rfile.fileno(), 0)
  209. os.dup2(self.wfile.fileno(), 1)
  210. os.execve(scriptfile, args, env)
  211. except:
  212. self.server.handle_error(self.request, self.client_address)
  213. os._exit(127)
  214. else:
  215. # Non Unix - use subprocess
  216. import subprocess
  217. cmdline = [scriptfile]
  218. if self.is_python(scriptfile):
  219. interp = sys.executable
  220. if interp.lower().endswith("w.exe"):
  221. # On Windows, use python.exe, not pythonw.exe
  222. interp = interp[:-5] + interp[-4:]
  223. cmdline = [interp, '-u'] + cmdline
  224. if '=' not in query:
  225. cmdline.append(query)
  226. self.log_message("command: %s", subprocess.list2cmdline(cmdline))
  227. try:
  228. nbytes = int(length)
  229. except (TypeError, ValueError):
  230. nbytes = 0
  231. p = subprocess.Popen(cmdline,
  232. stdin = subprocess.PIPE,
  233. stdout = subprocess.PIPE,
  234. stderr = subprocess.PIPE,
  235. env = env
  236. )
  237. if self.command.lower() == "post" and nbytes > 0:
  238. data = self.rfile.read(nbytes)
  239. else:
  240. data = None
  241. # throw away additional data [see bug #427345]
  242. while select.select([self.rfile._sock], [], [], 0)[0]:
  243. if not self.rfile._sock.recv(1):
  244. break
  245. stdout, stderr = p.communicate(data)
  246. self.wfile.write(stdout)
  247. if stderr:
  248. self.log_error('%s', stderr)
  249. p.stderr.close()
  250. p.stdout.close()
  251. status = p.returncode
  252. if status:
  253. self.log_error("CGI script exit status %#x", status)
  254. else:
  255. self.log_message("CGI script exited OK")
  256. def _url_collapse_path(path):
  257. """
  258. Given a URL path, remove extra '/'s and '.' path elements and collapse
  259. any '..' references and returns a colllapsed path.
  260. Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
  261. The utility of this function is limited to is_cgi method and helps
  262. preventing some security attacks.
  263. Returns: The reconstituted URL, which will always start with a '/'.
  264. Raises: IndexError if too many '..' occur within the path.
  265. """
  266. # Query component should not be involved.
  267. path, _, query = path.partition('?')
  268. path = urllib.unquote(path)
  269. # Similar to os.path.split(os.path.normpath(path)) but specific to URL
  270. # path semantics rather than local operating system semantics.
  271. path_parts = path.split('/')
  272. head_parts = []
  273. for part in path_parts[:-1]:
  274. if part == '..':
  275. head_parts.pop() # IndexError if more '..' than prior parts
  276. elif part and part != '.':
  277. head_parts.append( part )
  278. if path_parts:
  279. tail_part = path_parts.pop()
  280. if tail_part:
  281. if tail_part == '..':
  282. head_parts.pop()
  283. tail_part = ''
  284. elif tail_part == '.':
  285. tail_part = ''
  286. else:
  287. tail_part = ''
  288. if query:
  289. tail_part = '?'.join((tail_part, query))
  290. splitpath = ('/' + '/'.join(head_parts), tail_part)
  291. collapsed_path = "/".join(splitpath)
  292. return collapsed_path
  293. nobody = None
  294. def nobody_uid():
  295. """Internal routine to get nobody's uid"""
  296. global nobody
  297. if nobody:
  298. return nobody
  299. try:
  300. import pwd
  301. except ImportError:
  302. return -1
  303. try:
  304. nobody = pwd.getpwnam('nobody')[2]
  305. except KeyError:
  306. nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
  307. return nobody
  308. def executable(path):
  309. """Test for executable file."""
  310. try:
  311. st = os.stat(path)
  312. except os.error:
  313. return False
  314. return st.st_mode & 0111 != 0
  315. def test(HandlerClass = CGIHTTPRequestHandler,
  316. ServerClass = BaseHTTPServer.HTTPServer):
  317. SimpleHTTPServer.test(HandlerClass, ServerClass)
  318. if __name__ == '__main__':
  319. test()