95
89
self.headers = {} # Header names : values
97
def execute_cgi(interpreter, owner, jail_dir, working_dir, script_path,
98
req, gentle, overrides=None):
91
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
92
script_path, req, gentle):
100
94
trampoline: Full path on the local system to the CGI wrapper program
102
owner: User object of the owner of the file.
96
uid: User ID of the owner of the file.
103
97
jail_dir: Absolute path of owner's jail directory.
104
98
working_dir: Directory containing the script file relative to owner's
106
100
script_path: CGI script relative to the owner's jail.
107
101
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.
112
103
The called CGI wrapper application shall be called using popen and receive
113
104
the HTTP body on stdin. It shall receive the CGI environment variables to
117
trampoline = os.path.join(req.config['paths']['lib'], 'trampoline')
119
108
# Support no-op trampoline runs.
120
109
if interpreter is None:
121
110
interpreter = '/bin/true'
138
127
f.seek(0) # Rewind, for reading
140
129
# Set up the environment
141
environ = cgi_environ(req, script_path, owner, overrides=overrides)
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)
143
139
# usage: tramp uid jail_dir working_dir script_path
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,
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,
153
144
stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
154
cwd=tramp_dir, env=environ)
147
# Restore the environment
148
for k in os.environ.keys():
150
for (k,v) in old_env.items():
156
153
# We don't want any output! Bail out after the process terminates.
223
220
if len(split) == 1:
224
221
split = headers.split('\n', 1)
226
# If not executing in gentle mode (which presents CGI violations
227
# to users nicely), check if this an internal IVLE error
229
if not cgiflags.gentle:
230
hs = cgiflags.headers
231
if 'X-IVLE-Error-Type' in hs:
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'])
233
232
raise IVLEJailError(hs['X-IVLE-Error-Type'],
234
233
hs['X-IVLE-Error-Message'],
235
234
hs['X-IVLE-Error-Info'])
237
raise AssertionError("Bad error headers written by CGI.")
236
raise IVLEError(500, 'bad error headers written by CGI')
239
238
# Check to make sure the required headers were written
240
239
if cgiflags.wrote_html_warning or not cgiflags.gentle:
294
293
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)
317
296
# Read CGI headers
318
297
value = value.strip()
319
298
if name == "Content-Type":
364
343
""" % (warning, text))
345
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
366
347
# Mapping of interpreter names (as given in conf/app/server.py) to
367
348
# interpreter functions.
369
350
interpreter_objects = {
371
: functools.partial(execute_cgi, "/usr/bin/python"),
352
: functools.partial(execute_cgi, "/usr/bin/python",
353
location_cgi_python),
373
: functools.partial(execute_cgi, None),
355
: functools.partial(execute_cgi, None,
356
location_cgi_python),
374
357
# Should also have:
376
359
# python-server-page
379
def cgi_environ(req, script_path, user, overrides=None):
380
"""Gets CGI variables from apache and makes a few changes for security and
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.
383
366
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
369
# Comments here are on the heavy side, explained carefully for security
390
370
# 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():
397
372
# Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
398
373
# exposes unnecessary details about server.
439
414
# SERVER_SOFTWARE is actually not Apache but IVLE, since we are
440
415
# custom-making the CGI request.
441
env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
416
env['SERVER_SOFTWARE'] = "IVLE/" + str(ivle.conf.ivle_version)
443
418
# Additional environment variables
444
username = user.login
419
username = studpath.url_to_jailpaths(req.path)[0]
445
420
env['HOME'] = os.path.join('/home', username)
447
if overrides is not None:
448
env.update(overrides)
451
422
class ExecutionError(Exception):
454
def execute_raw(config, user, jail_dir, working_dir, binary, args):
425
def execute_raw(user, jail_dir, working_dir, binary, args):
455
426
'''Execute a binary in a user's jail, returning the raw output.
457
428
The binary is executed in the given working directory with the given
458
429
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]
432
tramp = location_cgi_python
433
tramp_dir = os.path.split(location_cgi_python)[0]
464
435
# 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,
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,
473
440
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
441
stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True)
442
exitcode = proc.wait()
483
444
if exitcode != 0:
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)
445
raise ExecutionError('subprocess ended with code %d, stderr %s' %
446
(exitcode, proc.stderr.read()))
447
return (proc.stdout.read(), proc.stderr.read())