~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/interpret.py

Update ivle.rpc.decorators to use new exceptions.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
# Runs a student script in a safe execution environment.
23
23
 
24
 
import ivle
25
24
from ivle import studpath
26
 
from ivle.util import IVLEJailError, split_path
 
25
from ivle.util import IVLEError, IVLEJailError
 
26
import ivle.conf
27
27
 
28
28
import functools
29
29
 
31
31
import pwd
32
32
import subprocess
33
33
import cgi
34
 
import StringIO
35
34
 
36
35
# TODO: Make progressive output work
37
36
# Question: Will having a large buffer size stop progressive output from
38
37
# working on smaller output
39
38
 
40
39
CGI_BLOCK_SIZE = 65535
41
 
PATH = "/usr/local/bin:/usr/bin:/bin"
42
40
 
43
 
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True,
44
 
    overrides=None):
 
41
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
45
42
    """Serves a file by interpreting it using one of IVLE's builtin
46
43
    interpreters. All interpreters are intended to run in the user's jail. The
47
44
    jail location is provided as an argument to the interpreter but it is up
52
49
    jail_dir: Absolute path to the user's jail.
53
50
    filename: Absolute filename within the user's jail.
54
51
    interpreter: A function object to call.
55
 
    gentle: ?
56
 
    overrides: A dict mapping env var names to strings, to override arbitrary
57
 
        environment variables in the resulting CGI environent.
58
52
    """
59
53
    # We can't test here whether or not the target file actually exists,
60
54
    # because the apache user may not have permission. Instead we have to
79
73
    # (Note that paths "relative" to the jail actually begin with a '/' as
80
74
    # they are absolute in the jailspace)
81
75
 
82
 
    return interpreter(owner, jail_dir, working_dir, filename_abs, req,
83
 
                       gentle, overrides=overrides)
 
76
    return interpreter(owner.unixid, jail_dir, working_dir, filename_abs, req,
 
77
                       gentle)
84
78
 
85
79
class CGIFlags:
86
80
    """Stores flags regarding the state of reading CGI output.
94
88
        self.linebuf = ""
95
89
        self.headers = {}       # Header names : values
96
90
 
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):
99
93
    """
100
94
    trampoline: Full path on the local system to the CGI wrapper program
101
95
        being executed.
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
105
99
        jail.
106
100
    script_path: CGI script relative to the owner's jail.
107
101
    req: IVLE request object.
108
 
    gentle: ?
109
 
    overrides: A dict mapping env var names to strings, to override arbitrary
110
 
        environment variables in the resulting CGI environent.
111
102
 
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
114
105
    its environment.
115
106
    """
116
107
 
117
 
    trampoline = os.path.join(req.config['paths']['lib'], 'trampoline')
118
 
 
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
139
128
 
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():
 
134
        del os.environ[k]
 
135
    for (k,v) in req.get_cgi_environ().items():
 
136
        os.environ[k] = v
 
137
    fixup_environ(req)
142
138
 
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)
151
 
                for s in cmd_line]
152
 
    pid = subprocess.Popen(cmd_line,
 
140
    pid = subprocess.Popen(
 
141
        [trampoline, str(uid), jail_dir, working_dir, interpreter,
 
142
        script_path],
153
143
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
154
 
        cwd=tramp_dir, env=environ)
 
144
        cwd=tramp_dir)
 
145
 
 
146
    # Restore the environment
 
147
    for k in os.environ.keys():
 
148
        del os.environ[k]
 
149
    for (k,v) in old_env.items():
 
150
        os.environ[k] = v
155
151
 
156
152
    # We don't want any output! Bail out after the process terminates.
157
153
    if noop:
223
219
            if len(split) == 1:
224
220
                split = headers.split('\n', 1)
225
221
 
226
 
        # If not executing in gentle mode (which presents CGI violations
227
 
        # to users nicely), check if this an internal IVLE error
228
 
        # condition.
229
 
        if not cgiflags.gentle:
230
 
            hs = cgiflags.headers
231
 
            if 'X-IVLE-Error-Type' in hs:
 
222
        # Is this an internal IVLE error condition?
 
223
        hs = cgiflags.headers
 
224
        if 'X-IVLE-Error-Type' in hs:
 
225
            t = hs['X-IVLE-Error-Type']
 
226
            if t == IVLEError.__name__:
 
227
                raise IVLEError(int(hs['X-IVLE-Error-Code']),
 
228
                                hs['X-IVLE-Error-Message'])
 
229
            else:
232
230
                try:
233
231
                    raise IVLEJailError(hs['X-IVLE-Error-Type'],
234
232
                                        hs['X-IVLE-Error-Message'],
235
233
                                        hs['X-IVLE-Error-Info'])
236
234
                except KeyError:
237
 
                    raise AssertionError("Bad error headers written by CGI.")
 
235
                    raise IVLEError(500, 'bad error headers written by CGI')
238
236
 
239
237
        # Check to make sure the required headers were written
240
238
        if cgiflags.wrote_html_warning or not cgiflags.gentle:
294
292
        process_cgi_output(req, line + '\n', cgiflags)
295
293
        return
296
294
 
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)):
301
 
        warning = "Warning"
302
 
        if not cgiflags.gentle:
303
 
            message = """An unexpected server error has occured."""
304
 
            warning = "Error"
305
 
        else:
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>( ) &lt; &gt; @ , ; : \\ " / [ ] ? = { } <em>SPACE 
310
 
            TAB</em></code>."""
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)
315
 
        return
316
 
 
317
295
    # Read CGI headers
318
296
    value = value.strip()
319
297
    if name == "Content-Type":
363
341
    <pre>
364
342
""" % (warning, text))
365
343
 
 
344
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
 
345
 
366
346
# Mapping of interpreter names (as given in conf/app/server.py) to
367
347
# interpreter functions.
368
348
 
369
349
interpreter_objects = {
370
350
    'cgi-python'
371
 
        : functools.partial(execute_cgi, "/usr/bin/python"),
 
351
        : functools.partial(execute_cgi, "/usr/bin/python",
 
352
            location_cgi_python),
372
353
    'noop'
373
 
        : functools.partial(execute_cgi, None),
 
354
        : functools.partial(execute_cgi, None,
 
355
            location_cgi_python),
374
356
    # Should also have:
375
357
    # cgi-generic
376
358
    # python-server-page
377
359
}
378
360
 
379
 
def cgi_environ(req, script_path, user, overrides=None):
380
 
    """Gets CGI variables from apache and makes a few changes for security and 
381
 
    correctness.
 
361
def fixup_environ(req):
 
362
    """Assuming os.environ has been written with the CGI variables from
 
363
    apache, make a few changes for security and correctness.
382
364
 
383
365
    Does not modify req, only reads it.
384
 
 
385
 
    overrides: A dict mapping env var names to strings, to override arbitrary
386
 
        environment variables in the resulting CGI environent.
387
366
    """
388
 
    env = {}
 
367
    env = os.environ
389
368
    # Comments here are on the heavy side, explained carefully for security
390
369
    # reasons. Please read carefully before making changes.
391
 
    
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():
395
 
        env[k] = v
396
370
 
397
371
    # Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
398
372
    # exposes unnecessary details about server.
410
384
        del env['PATH']
411
385
    except: pass
412
386
 
 
387
    # Remove SCRIPT_FILENAME. Not part of CGI spec (see SCRIPT_NAME).
 
388
 
 
389
    # PATH_INFO is wrong because the script doesn't physically exist.
 
390
    # Apache makes it relative to the "serve" app. It should actually be made
 
391
    # relative to the student's script. intepretservice does that in the jail,
 
392
    # so here we just clear it.
 
393
    env['PATH_INFO'] = ''
 
394
    env['PATH_TRANSLATED'] = ''
 
395
 
413
396
    # CGI specifies that REMOTE_HOST SHOULD be set, and MAY just be set to
414
397
    # REMOTE_ADDR. Since Apache does not appear to set this, set it to
415
398
    # REMOTE_ADDR.
416
399
    if 'REMOTE_HOST' not in env and 'REMOTE_ADDR' in env:
417
400
        env['REMOTE_HOST'] = env['REMOTE_ADDR']
418
401
 
419
 
    env['PATH_INFO'] = ''
420
 
    del env['PATH_TRANSLATED']
421
 
 
422
 
    normuri = os.path.normpath(req.uri)
423
 
    env['SCRIPT_NAME'] = normuri
424
 
 
425
402
    # SCRIPT_NAME is the path to the script WITHOUT PATH_INFO.
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)
431
 
 
432
 
        uri_into_jail = studpath.to_home_path(os.path.normpath(req.path))
433
 
 
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'])]
 
403
    script_name = req.uri
 
404
    env['SCRIPT_NAME'] = script_name
438
405
 
439
406
    # SERVER_SOFTWARE is actually not Apache but IVLE, since we are
440
407
    # custom-making the CGI request.
441
 
    env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
 
408
    env['SERVER_SOFTWARE'] = "IVLE/" + str(ivle.conf.ivle_version)
442
409
 
443
410
    # Additional environment variables
444
 
    username = user.login
 
411
    username = studpath.url_to_jailpaths(req.path)[0]
445
412
    env['HOME'] = os.path.join('/home', username)
446
413
 
447
 
    if overrides is not None:
448
 
        env.update(overrides)
449
 
    return env
450
 
 
451
414
class ExecutionError(Exception):
452
415
    pass
453
416
 
454
 
def execute_raw(config, user, jail_dir, working_dir, binary, args):
 
417
def execute_raw(user, jail_dir, working_dir, binary, args):
455
418
    '''Execute a binary in a user's jail, returning the raw output.
456
419
 
457
420
    The binary is executed in the given working directory with the given
458
421
    args. A tuple of (stdout, stderr) is returned.
459
422
    '''
460
423
 
461
 
    tramp = os.path.join(config['paths']['lib'], 'trampoline')
462
 
    tramp_dir = os.path.split(tramp)[0]
 
424
    tramp = location_cgi_python
 
425
    tramp_dir = os.path.split(location_cgi_python)[0]
463
426
 
464
427
    # 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)
471
 
                for s in cmd_line]
472
 
    proc = subprocess.Popen(cmd_line,
 
428
    proc = subprocess.Popen(
 
429
        [tramp, str(user.unixid), jail_dir, working_dir, binary] + args,
473
430
        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),
476
 
             'PATH': PATH,
477
 
             'USER': user.login,
478
 
             'LOGNAME': user.login})
479
 
 
480
 
    (stdout, stderr) = proc.communicate()
481
 
    exitcode = proc.returncode
 
431
        stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True)
 
432
    exitcode = proc.wait()
482
433
 
483
434
    if exitcode != 0:
484
 
        raise ExecutionError('subprocess ended with code %d, stderr: "%s"' %
485
 
                             (exitcode, stderr))
486
 
    return (stdout, stderr)
487
 
 
488
 
def jail_call(req, cgi_script, script_name, query_string=None,
489
 
    request_method="GET", extra_overrides=None):
490
 
    """
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.
494
 
 
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.
500
 
        eg: "/fileservice/"
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.
507
 
 
508
 
    Returns a triple (status_code, content_type, contents).
509
 
    """
510
 
    interp_object = interpreter_objects["cgi-python"]
511
 
    user_jail_dir = os.path.join(req.config['paths']['jails']['mounts'],
512
 
                                 req.user.login)
513
 
    overrides = {
514
 
        "SCRIPT_NAME": script_name,
515
 
        "QUERY_STRING": query_string,
516
 
        "REQUEST_URI": "%s%s%s" % (script_name, "?" if query_string else "",
517
 
                                   query_string),
518
 
        "REQUEST_METHOD": request_method,
519
 
    }
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()
526
 
 
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.)
532
 
    """
533
 
    def __init__(self, req):
534
 
        StringIO.StringIO.__init__(self)
535
 
        self._real_req = req
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)
 
435
        raise ExecutionError('subprocess ended with code %d, stderr %s' %
 
436
                             (exitcode, proc.stderr.read()))
 
437
    return (proc.stdout.read(), proc.stderr.read())