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

« back to all changes in this revision

Viewing changes to ivle/interpret.py

Quick port of fileservice to the new framework. It's still very much old-style,
though.

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, script_path)
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), ivle.conf.jail_base, ivle.conf.jail_src_base,
 
142
         ivle.conf.jail_system, jail_dir, working_dir, interpreter,
 
143
        script_path],
153
144
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
154
 
        cwd=tramp_dir, env=environ)
 
145
        cwd=tramp_dir)
 
146
 
 
147
    # Restore the environment
 
148
    for k in os.environ.keys():
 
149
        del os.environ[k]
 
150
    for (k,v) in old_env.items():
 
151
        os.environ[k] = v
155
152
 
156
153
    # We don't want any output! Bail out after the process terminates.
157
154
    if noop:
223
220
            if len(split) == 1:
224
221
                split = headers.split('\n', 1)
225
222
 
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:
 
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'])
 
230
            else:
232
231
                try:
233
232
                    raise IVLEJailError(hs['X-IVLE-Error-Type'],
234
233
                                        hs['X-IVLE-Error-Message'],
235
234
                                        hs['X-IVLE-Error-Info'])
236
235
                except KeyError:
237
 
                    raise AssertionError("Bad error headers written by CGI.")
 
236
                    raise IVLEError(500, 'bad error headers written by CGI')
238
237
 
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)
295
294
        return
296
295
 
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
296
    # Read CGI headers
318
297
    value = value.strip()
319
298
    if name == "Content-Type":
363
342
    <pre>
364
343
""" % (warning, text))
365
344
 
 
345
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
 
346
 
366
347
# Mapping of interpreter names (as given in conf/app/server.py) to
367
348
# interpreter functions.
368
349
 
369
350
interpreter_objects = {
370
351
    'cgi-python'
371
 
        : functools.partial(execute_cgi, "/usr/bin/python"),
 
352
        : functools.partial(execute_cgi, "/usr/bin/python",
 
353
            location_cgi_python),
372
354
    'noop'
373
 
        : functools.partial(execute_cgi, None),
 
355
        : functools.partial(execute_cgi, None,
 
356
            location_cgi_python),
374
357
    # Should also have:
375
358
    # cgi-generic
376
359
    # python-server-page
377
360
}
378
361
 
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.
 
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.
382
365
 
383
366
    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
367
    """
388
 
    env = {}
 
368
    env = os.environ
389
369
    # Comments here are on the heavy side, explained carefully for security
390
370
    # 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
371
 
397
372
    # Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
398
373
    # exposes unnecessary details about server.
429
404
    if script_path and script_path.startswith('/home'):
430
405
        normscript = os.path.normpath(script_path)
431
406
 
432
 
        uri_into_jail = studpath.to_home_path(os.path.normpath(req.path))
 
407
        uri_into_jail = studpath.url_to_jailpaths(os.path.normpath(req.path))[2]
433
408
 
434
409
        # PATH_INFO is wrong because the script doesn't physically exist.
435
410
        env['PATH_INFO'] = uri_into_jail[len(normscript):]
438
413
 
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)
442
417
 
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)
446
421
 
447
 
    if overrides is not None:
448
 
        env.update(overrides)
449
 
    return env
450
 
 
451
422
class ExecutionError(Exception):
452
423
    pass
453
424
 
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.
456
427
 
457
428
    The binary is executed in the given working directory with the given
458
429
    args. A tuple of (stdout, stderr) is returned.
459
430
    '''
460
431
 
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]
463
434
 
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)
471
 
                for s in cmd_line]
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),
476
 
             'PATH': PATH,
477
 
             'USER': user.login,
478
 
             'LOGNAME': user.login})
479
 
 
480
 
    (stdout, stderr) = proc.communicate()
481
 
    exitcode = proc.returncode
 
441
        stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True)
 
442
    exitcode = proc.wait()
482
443
 
483
444
    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)
 
445
        raise ExecutionError('subprocess ended with code %d, stderr %s' %
 
446
                             (exitcode, proc.stderr.read()))
 
447
    return (proc.stdout.read(), proc.stderr.read())