112
140
stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
143
# Restore the environment
144
for k in os.environ.keys():
146
for (k,v) in old_env.items():
115
149
# process_cgi_line: Reads a single line of CGI output and processes it.
116
150
# Prints to req, and also does fancy HTML warnings if Content-Type
119
cgiflags.got_cgi_header = False
120
cgiflags.started_cgi_body = False
121
cgiflags.wrote_html_warning = False
122
def process_cgi_line(line):
123
# FIXME? Issue with binary files (processing per-line?)
124
if cgiflags.started_cgi_body:
125
# FIXME: HTML escape text if wrote_html_warning
152
cgiflags = CGIFlags()
154
# Read from the process's stdout into req
155
data = pid.stdout.read(CGI_BLOCK_SIZE)
157
process_cgi_output(req, data, cgiflags)
158
data = pid.stdout.read(CGI_BLOCK_SIZE)
160
# If we haven't processed headers yet, now is a good time
161
if not cgiflags.started_cgi_body:
162
process_cgi_output(req, '\n', cgiflags)
164
# If we wrote an HTML warning header, write the footer
165
if cgiflags.wrote_html_warning:
171
def process_cgi_output(req, data, cgiflags):
172
"""Processes a chunk of CGI output. data is a string of arbitrary length;
173
some arbitrary chunk of output written by the CGI script."""
174
if cgiflags.started_cgi_body:
175
if cgiflags.wrote_html_warning:
176
# HTML escape text if wrote_html_warning
177
req.write(cgi.escape(data))
129
if line.strip() == "" and cgiflags.got_cgi_header:
130
cgiflags.started_cgi_body = True
131
elif line.startswith("Content-Type:"):
132
req.content_type = line[13:].strip()
133
cgiflags.got_cgi_header = True
134
elif line.startswith("Location:"):
136
cgiflags.got_cgi_header = True
137
elif line.startswith("Status:"):
139
cgiflags.got_cgi_header = True
140
elif cgiflags.got_cgi_header:
143
req.write("Invalid header")
181
# Break data into lines of CGI header data.
182
linebuf = cgiflags.linebuf + data
183
# First see if we can split all header data
184
split = linebuf.split('\r\n\r\n', 1)
186
# Allow UNIX newlines instead
187
split = linebuf.split('\n\n', 1)
189
# Haven't seen all headers yet. Buffer and come back later.
190
cgiflags.linebuf = linebuf
195
cgiflags.linebuf = ""
196
cgiflags.started_cgi_body = True
197
# Process all the header lines
198
split = headers.split('\r\n', 1)
200
split = headers.split('\n', 1)
202
process_cgi_header_line(req, split[0], cgiflags)
203
if len(split) == 1: break
205
if cgiflags.wrote_html_warning:
206
# We're done with headers. Treat the rest as data.
207
data = headers + '\n' + data
209
split = headers.split('\r\n', 1)
211
split = headers.split('\n', 1)
213
# Check to make sure the required headers were written
214
if cgiflags.wrote_html_warning:
215
# We already reported an error, that's enough
217
elif "Content-Type" in cgiflags.headers:
219
elif "Location" in cgiflags.headers:
220
if ("Status" in cgiflags.headers and req.status >= 300
221
and req.status < 400):
146
# Assume the user is not printing headers and give a warning
148
# User program did not print header.
149
# Make a fancy HTML warning for them.
150
req.content_type = "text/html"
151
req.write("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
224
message = """You did not write a valid status code for
225
the given location. To make a redirect, you may wish to try:</p>
226
<pre style="margin-left: 1em">Status: 302 Found
227
Location: <redirect address></pre>"""
228
write_html_warning(req, message)
229
cgiflags.wrote_html_warning = True
231
message = """You did not print a Content-Type header.
232
CGI requires that you print a "Content-Type". You may wish to try:</p>
233
<pre style="margin-left: 1em">Content-Type: text/html</pre>"""
234
write_html_warning(req, message)
235
cgiflags.wrote_html_warning = True
237
# Call myself to flush out the extra bit of data we read
238
process_cgi_output(req, data, cgiflags)
240
def process_cgi_header_line(req, line, cgiflags):
241
"""Process a line of CGI header data. line is a string representing a
242
complete line of text, stripped and without the newline.
245
name, value = line.split(':', 1)
247
# No colon. The user did not write valid headers.
248
if len(cgiflags.headers) == 0:
249
# First line was not a header line. We can assume this is not
251
message = """You did not print a CGI header.
252
CGI requires that you print a "Content-Type". You may wish to try:</p>
253
<pre style="margin-left: 1em">Content-Type: text/html</pre>"""
255
# They printed some header at least, but there was an invalid
257
message = """You printed an invalid CGI header. You need to leave
258
a blank line after the headers, before writing the page contents."""
259
write_html_warning(req, message)
260
cgiflags.wrote_html_warning = True
261
# Handle the rest of this line as normal data
262
process_cgi_output(req, line + '\n', cgiflags)
266
value = value.strip()
267
if name == "Content-Type":
268
req.content_type = value
269
elif name == "Location":
271
elif name == "Status":
272
# Must be an integer, followed by a space, and then the status line
273
# which we ignore (seems like Apache has no way to send a custom
276
req.status = int(value.split(' ', 1)[0])
278
message = """The "Status" CGI header was invalid. You need to
279
print a number followed by a message, such as "302 Found"."""
280
write_html_warning(req, message)
281
cgiflags.wrote_html_warning = True
282
# Handle the rest of this line as normal data
283
process_cgi_output(req, line + '\n', cgiflags)
285
# Generic HTTP header
286
# FIXME: Security risk letting users write arbitrary headers?
287
req.headers_out[name] = value
288
cgiflags.headers[name] = value
290
def write_html_warning(req, text):
291
"""Prints an HTML warning about invalid CGI interaction on the part of the
292
user. text may contain HTML markup."""
293
req.content_type = "text/html"
294
req.write("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
152
295
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
153
296
<html xmlns="http://www.w3.org/1999/xhtml">
155
298
<meta http-equiv="Content-Type"
156
299
content="text/html; charset=utf-8" />
158
<body style="margin: 0; padding: 0; font-family: sans;">
301
<body style="margin: 0; padding: 0; font-family: sans-serif;">
159
302
<div style="background-color: #faa; border-bottom: 1px solid black;
161
<p><strong>Warning</strong>: You did not print a "Content-Type" header.
162
CGI requires you to print some content type. You may wish to try:</p>
163
<pre style="margin-left: 1em">Content-Type: text/html</pre>
304
<p><strong>Warning</strong>: %s
165
306
<div style="margin: 8px;">
168
cgiflags.got_cgi_header = True
169
cgiflags.wrote_html_warning = True
170
cgiflags.started_cgi_body = True
173
# Read from the process's stdout into req
174
for line in pid.stdout:
175
process_cgi_line(line)
177
# If we wrote an HTML warning header, write the footer
178
if cgiflags.wrote_html_warning:
184
# TODO: Replace mytest with cgi trampoline handler script
185
310
location_cgi_python = os.path.join(conf.ivle_install_dir,
186
311
"bin/trampoline")
197
322
# python-server-page
325
def fixup_environ(req):
326
"""Assuming os.environ has been written with the CGI variables from
327
apache, make a few changes for security and correctness.
329
Does not modify req, only reads it.
332
# Comments here are on the heavy side, explained carefully for security
333
# reasons. Please read carefully before making changes.
335
# Remove HTTP_COOKIE. It is a security risk to have students see the IVLE
336
# cookie of their visitors.
338
del env['HTTP_COOKIE']
341
# Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
342
# exposes unnecessary details about server.
344
del env['DOCUMENT_ROOT']
347
del env['SCRIPT_FILENAME']
350
# Remove PATH. The PATH here is the path on the server machine; not useful
351
# inside the jail. It may be a good idea to add another path, reflecting
352
# the inside of the jail, but not done at this stage.
357
# Remove SCRIPT_FILENAME. Not part of CGI spec (see SCRIPT_NAME).
359
# PATH_INFO is wrong because the script doesn't physically exist.
360
# Apache makes it relative to the "serve" app. It should actually be made
361
# relative to the student's script.
362
# TODO: At this stage, it is not possible to add a path after the script,
363
# so PATH_INFO is always "".
365
env['PATH_INFO'] = path_info
367
# PATH_TRANSLATED currently points to a non-existant location within the
368
# local web server directory. Instead make it represent a path within the
370
(username, _, path_translated) = studpath.url_to_jailpaths(req.path)
371
if len(path_translated) == 0 or path_translated[0] != os.sep:
372
path_translated = os.sep + path_translated
373
env['PATH_TRANSLATED'] = path_translated
375
# CGI specifies that REMOTE_HOST SHOULD be set, and MAY just be set to
376
# REMOTE_ADDR. Since Apache does not appear to set this, set it to
378
if 'REMOTE_HOST' not in env and 'REMOTE_ADDR' in env:
379
env['REMOTE_HOST'] = env['REMOTE_ADDR']
381
# SCRIPT_NAME is the path to the script WITHOUT PATH_INFO.
382
script_name = req.uri
383
if len(path_info) > 0:
384
script_name = script_name[:-len(path_info)]
385
env['SCRIPT_NAME'] = script_name
387
# SERVER_SOFTWARE is actually not Apache but IVLE, since we are
388
# custom-making the CGI request.
389
env['SERVER_SOFTWARE'] = "IVLE/" + str(conf.ivle_version)
391
# Additional environment variables
392
env['HOME'] = os.path.join('/home', username)