36
36
# TODO: Make progressive output work
37
37
# Question: Will having a large buffer size stop progressive output from
38
38
# working on smaller output
40
40
CGI_BLOCK_SIZE = 65535
45
"""Get the unix uid corresponding to the given login name.
46
If it is not in the dictionary of uids, then consult the
47
database and retrieve an update of the user table."""
53
res = conn.get_all('login', ['login', 'unixid'])
55
return (flds['login'], flds['unixid'])
56
uids = dict(map(repack,res))
60
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
41
PATH = "/usr/local/bin:/usr/bin:/bin"
43
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True,
61
45
"""Serves a file by interpreting it using one of IVLE's builtin
62
46
interpreters. All interpreters are intended to run in the user's jail. The
63
47
jail location is provided as an argument to the interpreter but it is up
64
48
to the individual interpreters to create the jail.
66
50
req: An IVLE request object.
67
owner: Username of the user who owns the file being served.
51
owner: The user who owns the file being served.
68
52
jail_dir: Absolute path to the user's jail.
69
53
filename: Absolute filename within the user's jail.
70
54
interpreter: A function object to call.
56
overrides: A dict mapping env var names to strings, to override arbitrary
57
environment variables in the resulting CGI environent.
72
59
# We can't test here whether or not the target file actually exists,
73
60
# because the apache user may not have permission. Instead we have to
110
95
self.headers = {} # Header names : values
112
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
113
script_path, req, gentle):
97
def execute_cgi(interpreter, owner, jail_dir, working_dir, script_path,
98
req, gentle, overrides=None):
115
100
trampoline: Full path on the local system to the CGI wrapper program
117
uid: User ID of the owner of the file.
102
owner: User object of the owner of the file.
118
103
jail_dir: Absolute path of owner's jail directory.
119
104
working_dir: Directory containing the script file relative to owner's
121
106
script_path: CGI script relative to the owner's jail.
122
107
req: IVLE request object.
109
overrides: A dict mapping env var names to strings, to override arbitrary
110
environment variables in the resulting CGI environent.
124
112
The called CGI wrapper application shall be called using popen and receive
125
113
the HTTP body on stdin. It shall receive the CGI environment variables to
117
trampoline = os.path.join(req.config['paths']['lib'], 'trampoline')
129
119
# Support no-op trampoline runs.
130
120
if interpreter is None:
131
121
interpreter = '/bin/true'
148
138
f.seek(0) # Rewind, for reading
150
140
# Set up the environment
151
# This automatically asks mod_python to load up the CGI variables into the
152
# environment (which is a good first approximation)
153
old_env = os.environ.copy()
154
for k in os.environ.keys():
156
for (k,v) in req.get_cgi_environ().items():
141
environ = cgi_environ(req, script_path, owner, overrides=overrides)
160
143
# usage: tramp uid jail_dir working_dir script_path
161
pid = subprocess.Popen(
162
[trampoline, str(uid), jail_dir, working_dir, interpreter,
144
cmd_line = [trampoline, str(owner.unixid),
145
req.config['paths']['jails']['mounts'],
146
req.config['paths']['jails']['src'],
147
req.config['paths']['jails']['template'],
148
jail_dir, working_dir, interpreter, script_path]
149
# Popen doesn't like unicode strings. It hateses them.
150
cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
152
pid = subprocess.Popen(cmd_line,
164
153
stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
167
# Restore the environment
168
for k in os.environ.keys():
170
for (k,v) in old_env.items():
154
cwd=tramp_dir, env=environ)
173
156
# We don't want any output! Bail out after the process terminates.
313
294
process_cgi_output(req, line + '\n', cgiflags)
297
# Check if CGI field-name is valid
298
CGI_SEPERATORS = set(['(', ')', '<', '>', '@', ',', ';', ':', '\\', '"',
299
'/', '[', ']', '?', '=', '{', '}', ' ', '\t'])
300
if any((char in CGI_SEPERATORS for char in name)):
302
if not cgiflags.gentle:
303
message = """An unexpected server error has occured."""
306
# Header contained illegal characters
307
message = """You printed an invalid CGI header. CGI header
308
field-names can not contain any of the following characters:
309
<code>( ) < > @ , ; : \\ " / [ ] ? = { } <em>SPACE
311
write_html_warning(req, message, warning=warning)
312
cgiflags.wrote_html_warning = True
313
# Handle the rest of this line as normal data
314
process_cgi_output(req, line + '\n', cgiflags)
316
317
# Read CGI headers
317
318
value = value.strip()
318
319
if name == "Content-Type":
363
364
""" % (warning, text))
365
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
367
366
# Mapping of interpreter names (as given in conf/app/server.py) to
368
367
# interpreter functions.
370
369
interpreter_objects = {
372
: functools.partial(execute_cgi, "/usr/bin/python",
373
location_cgi_python),
371
: functools.partial(execute_cgi, "/usr/bin/python"),
375
: functools.partial(execute_cgi, None,
376
location_cgi_python),
373
: functools.partial(execute_cgi, None),
377
374
# Should also have:
379
376
# python-server-page
382
def fixup_environ(req):
383
"""Assuming os.environ has been written with the CGI variables from
384
apache, make a few changes for security and correctness.
379
def cgi_environ(req, script_path, user, overrides=None):
380
"""Gets CGI variables from apache and makes a few changes for security and
386
383
Does not modify req, only reads it.
385
overrides: A dict mapping env var names to strings, to override arbitrary
386
environment variables in the resulting CGI environent.
389
389
# Comments here are on the heavy side, explained carefully for security
390
390
# reasons. Please read carefully before making changes.
392
# This automatically asks mod_python to load up the CGI variables into the
393
# environment (which is a good first approximation)
394
for (k,v) in req.get_cgi_environ().items():
392
397
# Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
393
398
# exposes unnecessary details about server.
408
# Remove SCRIPT_FILENAME. Not part of CGI spec (see SCRIPT_NAME).
410
# PATH_INFO is wrong because the script doesn't physically exist.
411
# Apache makes it relative to the "serve" app. It should actually be made
412
# relative to the student's script. intepretservice does that in the jail,
413
# so here we just clear it.
414
env['PATH_INFO'] = ''
415
env['PATH_TRANSLATED'] = ''
417
413
# CGI specifies that REMOTE_HOST SHOULD be set, and MAY just be set to
418
414
# REMOTE_ADDR. Since Apache does not appear to set this, set it to
420
416
if 'REMOTE_HOST' not in env and 'REMOTE_ADDR' in env:
421
417
env['REMOTE_HOST'] = env['REMOTE_ADDR']
419
env['PATH_INFO'] = ''
420
del env['PATH_TRANSLATED']
422
normuri = os.path.normpath(req.uri)
423
env['SCRIPT_NAME'] = normuri
423
425
# SCRIPT_NAME is the path to the script WITHOUT PATH_INFO.
424
script_name = req.uri
425
env['SCRIPT_NAME'] = script_name
426
# We don't care about these if the script is null (ie. noop).
427
# XXX: We check for /home because we don't want to interfere with
428
# CGIRequest, which fileservice still uses.
429
if script_path and script_path.startswith('/home'):
430
normscript = os.path.normpath(script_path)
432
uri_into_jail = studpath.to_home_path(os.path.normpath(req.path))
434
# PATH_INFO is wrong because the script doesn't physically exist.
435
env['PATH_INFO'] = uri_into_jail[len(normscript):]
436
if len(env['PATH_INFO']) > 0:
437
env['SCRIPT_NAME'] = normuri[:-len(env['PATH_INFO'])]
427
439
# SERVER_SOFTWARE is actually not Apache but IVLE, since we are
428
440
# custom-making the CGI request.
429
env['SERVER_SOFTWARE'] = "IVLE/" + str(ivle.conf.ivle_version)
441
env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
431
443
# Additional environment variables
432
username = studpath.url_to_jailpaths(req.path)[0]
444
username = user.login
433
445
env['HOME'] = os.path.join('/home', username)
447
if overrides is not None:
448
env.update(overrides)
451
class ExecutionError(Exception):
454
def execute_raw(config, user, jail_dir, working_dir, binary, args):
455
'''Execute a binary in a user's jail, returning the raw output.
457
The binary is executed in the given working directory with the given
458
args. A tuple of (stdout, stderr) is returned.
461
tramp = os.path.join(config['paths']['lib'], 'trampoline')
462
tramp_dir = os.path.split(tramp)[0]
464
# Fire up trampoline. Vroom, vroom.
465
cmd_line = [tramp, str(user.unixid), config['paths']['jails']['mounts'],
466
config['paths']['jails']['src'],
467
config['paths']['jails']['template'],
468
jail_dir, working_dir, binary] + args
469
# Popen doesn't like unicode strings. It hateses them.
470
cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
472
proc = subprocess.Popen(cmd_line,
473
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
474
stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True,
475
env={'HOME': os.path.join('/home', user.login),
478
'LOGNAME': user.login})
480
(stdout, stderr) = proc.communicate()
481
exitcode = proc.returncode
484
raise ExecutionError('subprocess ended with code %d, stderr: "%s"' %
486
return (stdout, stderr)
488
def jail_call(req, cgi_script, script_name, query_string=None,
489
request_method="GET", extra_overrides=None):
491
Makes a call to a CGI script inside the jail from outside the jail.
492
This can be used to allow Python scripts to access jail-only functions and
493
data without having to perform a full API request.
495
req: A Request object (will not be written to or attributes modified).
496
cgi_script: Path to cgi script outside of jail.
497
eg: os.path.join(req.config['paths']['share'],
498
'services/fileservice')
499
script_name: Name to set as SCRIPT_NAME for the CGI environment.
501
query_string: Query string to set as QUERY_STRING for the CGI environment.
502
eg: "action=svnrepostat&path=/users/studenta/"
503
request_method: Method to set as REQUEST_METHOD for the CGI environment.
504
eg: "POST". Defaults to "GET".
505
extra_overrides: A dict mapping env var names to strings, to override
506
arbitrary environment variables in the resulting CGI environent.
508
Returns a triple (status_code, content_type, contents).
510
interp_object = interpreter_objects["cgi-python"]
511
user_jail_dir = os.path.join(req.config['paths']['jails']['mounts'],
514
"SCRIPT_NAME": script_name,
515
"QUERY_STRING": query_string,
516
"REQUEST_URI": "%s%s%s" % (script_name, "?" if query_string else "",
518
"REQUEST_METHOD": request_method,
520
if extra_overrides is not None:
521
overrides.update(extra_overrides)
522
result = DummyReq(req)
523
interpret_file(result, req.user, user_jail_dir, cgi_script, interp_object,
524
gentle=False, overrides=overrides)
525
return result.status, result.content_type, result.getvalue()
527
class DummyReq(StringIO.StringIO):
528
"""A dummy request object, built from a real request object, which can be
529
used like a req but doesn't mutate the existing request.
530
(Used for reading CGI responses as strings rather than forwarding their
531
output to the current request.)
533
def __init__(self, req):
534
StringIO.StringIO.__init__(self)
536
def get_cgi_environ(self):
537
return self._real_req.get_cgi_environ()
538
def __getattr__(self, name):
539
return getattr(self._real_req, name)