33This module builds on SimpleHTTPServer by implementing GET and POST
44requests to cgi-bin scripts.
55
6- If the os.fork() function is not present, this module will not work;
7- SystemError will be raised instead.
6+ If the os.fork() function is not present (e.g. on Windows),
7+ os.popen2() is used as a fallback, with slightly altered semantics; if
8+ that function is not present either (e.g. on Macintosh), only Python
9+ scripts are supported, and they are executed by the current process.
10+
11+ In all cases, the implementation is intentionally naive -- all
12+ requests are executed sychronously.
13+
14+ SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
15+ -- it may execute arbitrary Python code or external programs.
816
917"""
1018
1119
12- __version__ = "0.3 "
20+ __version__ = "0.4 "
1321
1422
1523import os
24+ import sys
1625import string
1726import urllib
1827import BaseHTTPServer
1928import SimpleHTTPServer
2029
2130
22- try :
23- os .fork
24- except AttributeError :
25- raise SystemError , __name__ + " requires os.fork()"
26-
27-
2831class CGIHTTPRequestHandler (SimpleHTTPServer .SimpleHTTPRequestHandler ):
2932
3033 """Complete HTTP server with GET, HEAD and POST commands.
@@ -35,6 +38,10 @@ class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
3538
3639 """
3740
41+ # Determine platform specifics
42+ have_fork = hasattr (os , 'fork' )
43+ have_popen2 = hasattr (os , 'popen2' )
44+
3845 # Make rfile unbuffered -- we need to read one line and then pass
3946 # the rest to a subprocess, so we can't use buffered input.
4047 rbufsize = 0
@@ -59,9 +66,9 @@ def send_head(self):
5966 return SimpleHTTPServer .SimpleHTTPRequestHandler .send_head (self )
6067
6168 def is_cgi (self ):
62- """test whether PATH corresponds to a CGI script.
69+ """Test whether self.path corresponds to a CGI script.
6370
64- Return a tuple (dir, rest) if PATH requires running a
71+ Return a tuple (dir, rest) if self.path requires running a
6572 CGI script, None if not. Note that rest begins with a
6673 slash if it is not empty.
6774
@@ -83,6 +90,15 @@ def is_cgi(self):
8390
8491 cgi_directories = ['/cgi-bin' , '/htbin' ]
8592
93+ def is_executable (self , path ):
94+ """Test whether argument path is an executable file."""
95+ return executable (path )
96+
97+ def is_python (self , path ):
98+ """Test whether argument path is a Python script."""
99+ head , tail = os .path .splitext (path )
100+ return tail .lower () in (".py" , ".pyw" )
101+
86102 def run_cgi (self ):
87103 """Execute a CGI script."""
88104 dir , rest = self .cgi_info
@@ -105,79 +121,152 @@ def run_cgi(self):
105121 self .send_error (403 , "CGI script is not a plain file (%s)" %
106122 `scriptname` )
107123 return
108- if not executable (scriptfile ):
109- self .send_error (403 , "CGI script is not executable (%s)" %
110- `scriptname` )
111- return
112- nobody = nobody_uid ()
124+ ispy = self .is_python (scriptname )
125+ if not ispy :
126+ if not (self .have_fork or self .have_popen2 ):
127+ self .send_error (403 , "CGI script is not a Python script (%s)" %
128+ `scriptname` )
129+ return
130+ if not self .is_executable (scriptfile ):
131+ self .send_error (403 , "CGI script is not executable (%s)" %
132+ `scriptname` )
133+ return
134+
135+ # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
136+ # XXX Much of the following could be prepared ahead of time!
137+ env = {}
138+ env ['SERVER_SOFTWARE' ] = self .version_string ()
139+ env ['SERVER_NAME' ] = self .server .server_name
140+ env ['GATEWAY_INTERFACE' ] = 'CGI/1.1'
141+ env ['SERVER_PROTOCOL' ] = self .protocol_version
142+ env ['SERVER_PORT' ] = str (self .server .server_port )
143+ env ['REQUEST_METHOD' ] = self .command
144+ uqrest = urllib .unquote (rest )
145+ env ['PATH_INFO' ] = uqrest
146+ env ['PATH_TRANSLATED' ] = self .translate_path (uqrest )
147+ env ['SCRIPT_NAME' ] = scriptname
148+ if query :
149+ env ['QUERY_STRING' ] = query
150+ host = self .address_string ()
151+ if host != self .client_address [0 ]:
152+ env ['REMOTE_HOST' ] = host
153+ env ['REMOTE_ADDR' ] = self .client_address [0 ]
154+ # XXX AUTH_TYPE
155+ # XXX REMOTE_USER
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+ accept = []
165+ for line in self .headers .getallmatchingheaders ('accept' ):
166+ if line [:1 ] in string .whitespace :
167+ accept .append (string .strip (line ))
168+ else :
169+ accept = accept + string .split (line [7 :], ',' )
170+ env ['HTTP_ACCEPT' ] = string .joinfields (accept , ',' )
171+ ua = self .headers .getheader ('user-agent' )
172+ if ua :
173+ env ['HTTP_USER_AGENT' ] = ua
174+ co = filter (None , self .headers .getheaders ('cookie' ))
175+ if co :
176+ env ['HTTP_COOKIE' ] = string .join (co , ', ' )
177+ # XXX Other HTTP_* headers
178+ if not self .have_fork :
179+ # Since we're setting the env in the parent, provide empty
180+ # values to override previously set values
181+ for k in ('QUERY_STRING' , 'REMOTE_HOST' , 'CONTENT_LENGTH' ,
182+ 'HTTP_USER_AGENT' , 'HTTP_COOKIE' ):
183+ env .setdefault (k , "" )
184+
113185 self .send_response (200 , "Script output follows" )
114- self .wfile .flush () # Always flush before forking
115- pid = os .fork ()
116- if pid != 0 :
117- # Parent
118- pid , sts = os .waitpid (pid , 0 )
186+
187+ decoded_query = string .replace (query , '+' , ' ' )
188+
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+ if sts :
201+ self .log_error ("CGI script exit status %#x" , sts )
202+ return
203+ # Child
204+ try :
205+ try :
206+ os .setuid (nobody )
207+ except os .error :
208+ pass
209+ os .dup2 (self .rfile .fileno (), 0 )
210+ os .dup2 (self .wfile .fileno (), 1 )
211+ os .execve (scriptfile , args , env )
212+ except :
213+ self .server .handle_error (self .request , self .client_address )
214+ os ._exit (127 )
215+
216+ elif self .have_popen2 :
217+ # Windows -- use popen2 to create a subprocess
218+ import shutil
219+ os .environ .update (env )
220+ cmdline = scriptfile
221+ if self .is_python (scriptfile ):
222+ interp = sys .executable
223+ if interp .lower ().endswith ("w.exe" ):
224+ # On Windows, use python.exe, not python.exe
225+ interp = interp [:- 5 ] = interp [- 4 :]
226+ cmdline = "%s %s" % (interp , cmdline )
227+ if '=' not in query and '"' not in query :
228+ cmdline = '%s "%s"' % (cmdline , query )
229+ self .log_error ("command: %s" , cmdline )
230+ try :
231+ nbytes = int (length )
232+ except :
233+ nbytes = 0
234+ fi , fo = os .popen2 (cmdline )
235+ if self .command .lower () == "post" and nbytes > 0 :
236+ data = self .rfile .read (nbytes )
237+ fi .write (data )
238+ fi .close ()
239+ shutil .copyfileobj (fo , self .wfile )
240+ sts = fo .close ()
119241 if sts :
120- self .log_error ("CGI script exit status x%x" % sts )
121- return
122- # Child
123- try :
124- # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
125- # XXX Much of the following could be prepared ahead of time!
126- env = {}
127- env ['SERVER_SOFTWARE' ] = self .version_string ()
128- env ['SERVER_NAME' ] = self .server .server_name
129- env ['GATEWAY_INTERFACE' ] = 'CGI/1.1'
130- env ['SERVER_PROTOCOL' ] = self .protocol_version
131- env ['SERVER_PORT' ] = str (self .server .server_port )
132- env ['REQUEST_METHOD' ] = self .command
133- uqrest = urllib .unquote (rest )
134- env ['PATH_INFO' ] = uqrest
135- env ['PATH_TRANSLATED' ] = self .translate_path (uqrest )
136- env ['SCRIPT_NAME' ] = scriptname
137- if query :
138- env ['QUERY_STRING' ] = query
139- host = self .address_string ()
140- if host != self .client_address [0 ]:
141- env ['REMOTE_HOST' ] = host
142- env ['REMOTE_ADDR' ] = self .client_address [0 ]
143- # AUTH_TYPE
144- # REMOTE_USER
145- # REMOTE_IDENT
146- if self .headers .typeheader is None :
147- env ['CONTENT_TYPE' ] = self .headers .type
242+ self .log_error ("CGI script exit status %#x" , sts )
148243 else :
149- env ['CONTENT_TYPE' ] = self .headers .typeheader
150- length = self .headers .getheader ('content-length' )
151- if length :
152- env ['CONTENT_LENGTH' ] = length
153- accept = []
154- for line in self .headers .getallmatchingheaders ('accept' ):
155- if line [:1 ] in string .whitespace :
156- accept .append (string .strip (line ))
157- else :
158- accept = accept + string .split (line [7 :], ',' )
159- env ['HTTP_ACCEPT' ] = string .joinfields (accept , ',' )
160- ua = self .headers .getheader ('user-agent' )
161- if ua :
162- env ['HTTP_USER_AGENT' ] = ua
163- co = filter (None , self .headers .getheaders ('cookie' ))
164- if co :
165- env ['HTTP_COOKIE' ] = string .join (co , ', ' )
166- # XXX Other HTTP_* headers
167- decoded_query = string .replace (query , '+' , ' ' )
244+ self .log_error ("CGI script exited OK" )
245+
246+ else :
247+ # Other O.S. -- execute script in this process
248+ os .environ .update (env )
249+ save_argv = sys .argv
250+ save_stdin = sys .stdin
251+ save_stdout = sys .stdout
252+ save_stderr = sys .stderr
168253 try :
169- os .setuid (nobody )
170- except os .error :
171- pass
172- os .dup2 (self .rfile .fileno (), 0 )
173- os .dup2 (self .wfile .fileno (), 1 )
174- print scriptfile , script , decoded_query
175- os .execve (scriptfile ,
176- [script , decoded_query ],
177- env )
178- except :
179- self .server .handle_error (self .request , self .client_address )
180- os ._exit (127 )
254+ try :
255+ sys .argv = [scriptfile ]
256+ if '=' not in decoded_query :
257+ sys .argv .append (decoded_query )
258+ sys .stdout = self .wfile
259+ sys .stdin = self .rfile
260+ execfile (scriptfile , {"__name__" : "__main__" })
261+ finally :
262+ sys .argv = save_argv
263+ sys .stdin = save_stdin
264+ sys .stdout = save_stdout
265+ sys .stderr = save_stderr
266+ except SystemExit , sts :
267+ self .log_error ("CGI script exit status %s" , str (sts ))
268+ else :
269+ self .log_error ("CGI script exited OK" )
181270
182271
183272nobody = None
@@ -187,7 +276,10 @@ def nobody_uid():
187276 global nobody
188277 if nobody :
189278 return nobody
190- import pwd
279+ try :
280+ import pwd
281+ except ImportError :
282+ return - 1
191283 try :
192284 nobody = pwd .getpwnam ('nobody' )[2 ]
193285 except KeyError :
0 commit comments