22
22
# Runs a student script in a safe execution environment.
24
# NOTE: This script currently disables cookies. This means students will be
25
# unable to write session-based or stateful web applications. This is done for
26
# security reasons (we do not want the students to see the IVLE cookie of
27
# whoever is visiting their pages).
28
# This can be resolved but needs careful sanitisation. See fixup_environ.
30
24
from common import studpath
26
from common.util import IVLEError, IVLEJailError
43
39
CGI_BLOCK_SIZE = 65535
45
def interpret_file(req, owner, filename, interpreter):
44
"""Get the unix uid corresponding to the given login name.
45
If it is not in the dictionary of uids, then consult the
46
database and retrieve an update of the user table."""
52
res = conn.get_all('login', ['login', 'unixid'])
54
return (flds['login'], flds['unixid'])
55
uids = dict(map(repack,res))
59
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
46
60
"""Serves a file by interpreting it using one of IVLE's builtin
47
61
interpreters. All interpreters are intended to run in the user's jail. The
48
62
jail location is provided as an argument to the interpreter but it is up
51
65
req: An IVLE request object.
52
66
owner: Username of the user who owns the file being served.
53
filename: Filename in the local file system.
67
jail_dir: Absolute path to the user's jail.
68
filename: Absolute filename within the user's jail.
54
69
interpreter: A function object to call.
56
# Make sure the file exists (otherwise some interpreters may not actually
58
# Don't test for execute permission, that will only be required for
59
# certain interpreters.
60
if not os.access(filename, os.R_OK):
61
req.throw_error(req.HTTP_NOT_FOUND)
71
# We can't test here whether or not the target file actually exists,
72
# because the apache user may not have permission. Instead we have to
73
# rely on the interpreter generating an error.
74
if filename.startswith(os.sep):
75
filename_abs = filename
76
filename_rel = filename[1:]
78
filename_abs = os.path.join(os.sep, filename)
79
filename_rel = filename
63
81
# Get the UID of the owner of the file
64
82
# (Note: files are executed by their owners, not the logged in user.
65
83
# This ensures users are responsible for their own programs and also
66
84
# allows them to be executed by the public).
68
(_,_,uid,_,_,_,_) = pwd.getpwnam(owner)
70
# The user does not exist. This should have already failed the
72
req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR)
74
87
# Split up req.path again, this time with respect to the jail
75
(_, jail_dir, path) = studpath.url_to_jailpaths(req.path)
76
path = os.path.join('/', path)
77
(working_dir, _) = os.path.split(path)
88
(working_dir, _) = os.path.split(filename_abs)
78
89
# jail_dir is the absolute jail directory.
79
90
# path is the filename relative to the user's jail.
80
91
# working_dir is the directory containing the file relative to the user's
82
93
# (Note that paths "relative" to the jail actually begin with a '/' as
83
94
# they are absolute in the jailspace)
85
return interpreter(uid, jail_dir, working_dir, path, req)
96
return interpreter(uid, jail_dir, working_dir, filename_abs, req,
88
"""Stores flags regarding the state of reading CGI output."""
100
"""Stores flags regarding the state of reading CGI output.
101
If this is to be gentle, detection of invalid headers will result in an
103
def __init__(self, begentle=True):
104
self.gentle = begentle
90
105
self.started_cgi_body = False
91
106
self.got_cgi_headers = False
92
107
self.wrote_html_warning = False
94
109
self.headers = {} # Header names : values
96
111
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
112
script_path, req, gentle):
99
114
trampoline: Full path on the local system to the CGI wrapper program
146
169
for (k,v) in old_env.items():
147
170
os.environ[k] = v
172
# We don't want any output! Bail out after the process terminates.
149
177
# process_cgi_line: Reads a single line of CGI output and processes it.
150
178
# Prints to req, and also does fancy HTML warnings if Content-Type
152
cgiflags = CGIFlags()
180
cgiflags = CGIFlags(gentle)
154
182
# Read from the process's stdout into req
155
183
data = pid.stdout.read(CGI_BLOCK_SIZE)
181
209
# Break data into lines of CGI header data.
182
210
linebuf = cgiflags.linebuf + data
183
211
# 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)
212
# We need to get the double CRLF- or LF-terminated headers, whichever
213
# is smaller, as either sequence may appear somewhere in the body.
214
usplit = linebuf.split('\n\n', 1)
215
wsplit = linebuf.split('\r\n\r\n', 1)
216
split = len(usplit[0]) > len(wsplit[0]) and wsplit or usplit
188
217
if len(split) == 1:
189
218
# Haven't seen all headers yet. Buffer and come back later.
190
219
cgiflags.linebuf = linebuf
210
239
if len(split) == 1:
211
240
split = headers.split('\n', 1)
242
# Is this an internal IVLE error condition?
243
hs = cgiflags.headers
244
if 'X-IVLE-Error-Type' in hs:
245
t = hs['X-IVLE-Error-Type']
246
if t == IVLEError.__name__:
247
raise IVLEError(int(hs['X-IVLE-Error-Code']),
248
hs['X-IVLE-Error-Message'])
251
raise IVLEJailError(hs['X-IVLE-Error-Type'],
252
hs['X-IVLE-Error-Message'],
253
hs['X-IVLE-Error-Info'])
255
raise IVLEError(500, 'bad error headers written by CGI')
213
257
# Check to make sure the required headers were written
214
if cgiflags.wrote_html_warning:
258
if cgiflags.wrote_html_warning or not cgiflags.gentle:
215
259
# We already reported an error, that's enough
217
261
elif "Content-Type" in cgiflags.headers:
245
289
name, value = line.split(':', 1)
246
290
except ValueError:
247
291
# No colon. The user did not write valid headers.
248
if len(cgiflags.headers) == 0:
292
# If we are being gentle, we want to help the user understand what
293
# went wrong. Otherwise, just admit we screwed up.
295
if not cgiflags.gentle:
296
message = """An unexpected server error has occured."""
298
elif len(cgiflags.headers) == 0:
249
299
# First line was not a header line. We can assume this is not
251
301
message = """You did not print a CGI header.
257
307
message = """You printed an invalid CGI header. You need to leave
258
308
a blank line after the headers, before writing the page contents."""
259
write_html_warning(req, message)
309
write_html_warning(req, message, warning=warning)
260
310
cgiflags.wrote_html_warning = True
261
311
# Handle the rest of this line as normal data
262
312
process_cgi_output(req, line + '\n', cgiflags)
276
326
req.status = int(value.split(' ', 1)[0])
277
327
except ValueError:
328
if not cgiflags.gentle:
329
# This isn't user code, so it should be good.
330
# Get us out of here!
278
332
message = """The "Status" CGI header was invalid. You need to
279
333
print a number followed by a message, such as "302 Found"."""
280
334
write_html_warning(req, message)
285
339
# Generic HTTP header
286
340
# FIXME: Security risk letting users write arbitrary headers?
287
req.headers_out[name] = value
288
cgiflags.headers[name] = value
341
req.headers_out.add(name, value)
342
cgiflags.headers[name] = value # FIXME: Only the last header will end up here.
290
def write_html_warning(req, text):
344
def write_html_warning(req, text, warning="Warning"):
291
345
"""Prints an HTML warning about invalid CGI interaction on the part of the
292
346
user. text may contain HTML markup."""
293
347
req.content_type = "text/html"
301
355
<body style="margin: 0; padding: 0; font-family: sans-serif;">
302
356
<div style="background-color: #faa; border-bottom: 1px solid black;
304
<p><strong>Warning</strong>: %s
358
<p><strong>%s</strong>: %s
306
360
<div style="margin: 8px;">
362
""" % (warning, text))
310
364
location_cgi_python = os.path.join(conf.ivle_install_dir,
311
365
"bin/trampoline")
332
389
# Comments here are on the heavy side, explained carefully for security
333
390
# 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
392
# Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
342
393
# exposes unnecessary details about server.