37
37
# working on smaller output
39
39
CGI_BLOCK_SIZE = 65535
40
PATH = "/usr/local/bin:/usr/bin:/bin"
41
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
42
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True,
42
44
"""Serves a file by interpreting it using one of IVLE's builtin
43
45
interpreters. All interpreters are intended to run in the user's jail. The
44
46
jail location is provided as an argument to the interpreter but it is up
49
51
jail_dir: Absolute path to the user's jail.
50
52
filename: Absolute filename within the user's jail.
51
53
interpreter: A function object to call.
55
overrides: A dict mapping env var names to strings, to override arbitrary
56
environment variables in the resulting CGI environent.
53
58
# We can't test here whether or not the target file actually exists,
54
59
# because the apache user may not have permission. Instead we have to
73
78
# (Note that paths "relative" to the jail actually begin with a '/' as
74
79
# they are absolute in the jailspace)
76
return interpreter(owner.unixid, jail_dir, working_dir, filename_abs, req,
81
return interpreter(owner, jail_dir, working_dir, filename_abs, req,
82
gentle, overrides=overrides)
80
85
"""Stores flags regarding the state of reading CGI output.
89
94
self.headers = {} # Header names : values
91
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
92
script_path, req, gentle):
96
def execute_cgi(interpreter, owner, jail_dir, working_dir, script_path,
97
req, gentle, overrides=None):
94
99
trampoline: Full path on the local system to the CGI wrapper program
96
uid: User ID of the owner of the file.
101
owner: User object of the owner of the file.
97
102
jail_dir: Absolute path of owner's jail directory.
98
103
working_dir: Directory containing the script file relative to owner's
100
105
script_path: CGI script relative to the owner's jail.
101
106
req: IVLE request object.
108
overrides: A dict mapping env var names to strings, to override arbitrary
109
environment variables in the resulting CGI environent.
103
111
The called CGI wrapper application shall be called using popen and receive
104
112
the HTTP body on stdin. It shall receive the CGI environment variables to
116
trampoline = os.path.join(req.config['paths']['lib'], 'trampoline')
108
118
# Support no-op trampoline runs.
109
119
if interpreter is None:
110
120
interpreter = '/bin/true'
127
137
f.seek(0) # Rewind, for reading
129
139
# Set up the environment
130
# This automatically asks mod_python to load up the CGI variables into the
131
# environment (which is a good first approximation)
132
old_env = os.environ.copy()
133
for k in os.environ.keys():
135
for (k,v) in req.get_cgi_environ().items():
137
fixup_environ(req, script_path)
140
environ = cgi_environ(req, script_path, owner, overrides=overrides)
139
142
# usage: tramp uid jail_dir working_dir script_path
140
pid = subprocess.Popen(
141
[trampoline, str(uid), ivle.conf.jail_base, ivle.conf.jail_src_base,
142
ivle.conf.jail_system, jail_dir, working_dir, interpreter,
143
cmd_line = [trampoline, str(owner.unixid),
144
req.config['paths']['jails']['mounts'],
145
req.config['paths']['jails']['src'],
146
req.config['paths']['jails']['template'],
147
jail_dir, working_dir, interpreter, script_path]
148
# Popen doesn't like unicode strings. It hateses them.
149
cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
151
pid = subprocess.Popen(cmd_line,
144
152
stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
147
# Restore the environment
148
for k in os.environ.keys():
150
for (k,v) in old_env.items():
153
cwd=tramp_dir, env=environ)
153
155
# We don't want any output! Bail out after the process terminates.
220
222
if len(split) == 1:
221
223
split = headers.split('\n', 1)
223
# Is this an internal IVLE error condition?
224
hs = cgiflags.headers
225
if 'X-IVLE-Error-Type' in hs:
226
t = hs['X-IVLE-Error-Type']
227
if t == IVLEError.__name__:
228
raise IVLEError(int(hs['X-IVLE-Error-Code']),
229
hs['X-IVLE-Error-Message'])
225
# If not executing in gentle mode (which presents CGI violations
226
# to users nicely), check if this an internal IVLE error
228
if not cgiflags.gentle:
229
hs = cgiflags.headers
230
if 'X-IVLE-Error-Type' in hs:
232
232
raise IVLEJailError(hs['X-IVLE-Error-Type'],
233
233
hs['X-IVLE-Error-Message'],
234
234
hs['X-IVLE-Error-Info'])
236
raise IVLEError(500, 'bad error headers written by CGI')
236
raise AssertionError("Bad error headers written by CGI.")
238
238
# Check to make sure the required headers were written
239
239
if cgiflags.wrote_html_warning or not cgiflags.gentle:
293
293
process_cgi_output(req, line + '\n', cgiflags)
296
# Check if CGI field-name is valid
297
CGI_SEPERATORS = set(['(', ')', '<', '>', '@', ',', ';', ':', '\\', '"',
298
'/', '[', ']', '?', '=', '{', '}', ' ', '\t'])
299
if any((char in CGI_SEPERATORS for char in name)):
301
if not cgiflags.gentle:
302
message = """An unexpected server error has occured."""
305
# Header contained illegal characters
306
message = """You printed an invalid CGI header. CGI header
307
field-names can not contain any of the following characters:
308
<code>( ) < > @ , ; : \\ " / [ ] ? = { } <em>SPACE
310
write_html_warning(req, message, warning=warning)
311
cgiflags.wrote_html_warning = True
312
# Handle the rest of this line as normal data
313
process_cgi_output(req, line + '\n', cgiflags)
296
316
# Read CGI headers
297
317
value = value.strip()
298
318
if name == "Content-Type":
343
363
""" % (warning, text))
345
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
347
365
# Mapping of interpreter names (as given in conf/app/server.py) to
348
366
# interpreter functions.
350
368
interpreter_objects = {
352
: functools.partial(execute_cgi, "/usr/bin/python",
353
location_cgi_python),
370
: functools.partial(execute_cgi, "/usr/bin/python"),
355
: functools.partial(execute_cgi, None,
356
location_cgi_python),
372
: functools.partial(execute_cgi, None),
357
373
# Should also have:
359
375
# python-server-page
362
def fixup_environ(req, script_path):
363
"""Assuming os.environ has been written with the CGI variables from
364
apache, make a few changes for security and correctness.
378
def cgi_environ(req, script_path, user, overrides=None):
379
"""Gets CGI variables from apache and makes a few changes for security and
366
382
Does not modify req, only reads it.
384
overrides: A dict mapping env var names to strings, to override arbitrary
385
environment variables in the resulting CGI environent.
369
388
# Comments here are on the heavy side, explained carefully for security
370
389
# reasons. Please read carefully before making changes.
391
# This automatically asks mod_python to load up the CGI variables into the
392
# environment (which is a good first approximation)
393
for (k,v) in req.get_cgi_environ().items():
372
396
# Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
373
397
# exposes unnecessary details about server.
414
438
# SERVER_SOFTWARE is actually not Apache but IVLE, since we are
415
439
# custom-making the CGI request.
416
env['SERVER_SOFTWARE'] = "IVLE/" + str(ivle.conf.ivle_version)
440
env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
418
442
# Additional environment variables
419
username = split_path(req.path)[0]
443
username = user.login
420
444
env['HOME'] = os.path.join('/home', username)
446
if overrides is not None:
447
env.update(overrides)
422
450
class ExecutionError(Exception):
425
def execute_raw(user, jail_dir, working_dir, binary, args):
453
def execute_raw(config, user, jail_dir, working_dir, binary, args):
426
454
'''Execute a binary in a user's jail, returning the raw output.
428
456
The binary is executed in the given working directory with the given
429
457
args. A tuple of (stdout, stderr) is returned.
432
tramp = location_cgi_python
433
tramp_dir = os.path.split(location_cgi_python)[0]
460
tramp = os.path.join(config['paths']['lib'], 'trampoline')
461
tramp_dir = os.path.split(tramp)[0]
435
463
# Fire up trampoline. Vroom, vroom.
436
proc = subprocess.Popen(
437
[tramp, str(user.unixid), ivle.conf.jail_base,
438
ivle.conf.jail_src_base, ivle.conf.jail_system, jail_dir,
439
working_dir, binary] + args,
464
cmd_line = [tramp, str(user.unixid), config['paths']['jails']['mounts'],
465
config['paths']['jails']['src'],
466
config['paths']['jails']['template'],
467
jail_dir, working_dir, binary] + args
468
# Popen doesn't like unicode strings. It hateses them.
469
cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
471
proc = subprocess.Popen(cmd_line,
440
472
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
441
stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True)
473
stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True,
474
env={'HOME': os.path.join('/home', user.login),
477
'LOGNAME': user.login})
443
479
(stdout, stderr) = proc.communicate()
444
480
exitcode = proc.returncode
446
482
if exitcode != 0:
447
raise ExecutionError('subprocess ended with code %d, stderr %s' %
448
(exitcode, proc.stderr.read()))
483
raise ExecutionError('subprocess ended with code %d, stderr: "%s"' %
449
485
return (stdout, stderr)