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

« back to all changes in this revision

Viewing changes to ivle/interpret.py

  • Committer: William Grant
  • Date: 2010-07-03 08:18:50 UTC
  • Revision ID: grantw@unimelb.edu.au-20100703081850-ygl8vp5nyznf3dnk
Trailing slashes in URLs can now be detected by views accepting subpaths.

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
24
25
from ivle import studpath
25
 
from ivle import db
26
 
from ivle.util import IVLEError, IVLEJailError
27
 
import ivle.conf
 
26
from ivle.util import IVLEJailError, split_path
28
27
 
29
28
import functools
30
29
 
38
37
# working on smaller output
39
38
 
40
39
CGI_BLOCK_SIZE = 65535
41
 
 
42
 
uids = {}
43
 
 
44
 
def get_uid(login):
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."""
48
 
    global uids
49
 
    if login in uids:
50
 
        return uids[login]
51
 
 
52
 
    conn = db.DB()
53
 
    res = conn.get_all('login', ['login', 'unixid'])
54
 
    def repack(flds):
55
 
        return (flds['login'], flds['unixid'])
56
 
    uids = dict(map(repack,res))
57
 
 
58
 
    return uids[login]
 
40
PATH = "/usr/local/bin:/usr/bin:/bin"
59
41
 
60
42
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
61
43
    """Serves a file by interpreting it using one of IVLE's builtin
64
46
    to the individual interpreters to create the jail.
65
47
 
66
48
    req: An IVLE request object.
67
 
    owner: Username of the user who owns the file being served.
 
49
    owner: The user who owns the file being served.
68
50
    jail_dir: Absolute path to the user's jail.
69
51
    filename: Absolute filename within the user's jail.
70
52
    interpreter: A function object to call.
79
61
        filename_abs = os.path.join(os.sep, filename)
80
62
        filename_rel = filename
81
63
 
82
 
    # Get the UID of the owner of the file
83
64
    # (Note: files are executed by their owners, not the logged in user.
84
65
    # This ensures users are responsible for their own programs and also
85
66
    # allows them to be executed by the public).
86
 
    uid = get_uid(owner)
87
67
 
88
68
    # Split up req.path again, this time with respect to the jail
89
69
    (working_dir, _) = os.path.split(filename_abs)
94
74
    # (Note that paths "relative" to the jail actually begin with a '/' as
95
75
    # they are absolute in the jailspace)
96
76
 
97
 
    return interpreter(uid, jail_dir, working_dir, filename_abs, req,
 
77
    return interpreter(owner, jail_dir, working_dir, filename_abs, req,
98
78
                       gentle)
99
79
 
100
80
class CGIFlags:
109
89
        self.linebuf = ""
110
90
        self.headers = {}       # Header names : values
111
91
 
112
 
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
113
 
                script_path, req, gentle):
 
92
def execute_cgi(interpreter, owner, jail_dir, working_dir, script_path,
 
93
                req, gentle):
114
94
    """
115
95
    trampoline: Full path on the local system to the CGI wrapper program
116
96
        being executed.
117
 
    uid: User ID of the owner of the file.
 
97
    owner: User object of the owner of the file.
118
98
    jail_dir: Absolute path of owner's jail directory.
119
99
    working_dir: Directory containing the script file relative to owner's
120
100
        jail.
126
106
    its environment.
127
107
    """
128
108
 
 
109
    trampoline = os.path.join(req.config['paths']['lib'], 'trampoline')
 
110
 
129
111
    # Support no-op trampoline runs.
130
112
    if interpreter is None:
131
113
        interpreter = '/bin/true'
148
130
        f.seek(0)       # Rewind, for reading
149
131
 
150
132
    # 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():
155
 
        del os.environ[k]
156
 
    for (k,v) in req.get_cgi_environ().items():
157
 
        os.environ[k] = v
158
 
    fixup_environ(req)
 
133
    environ = cgi_environ(req, script_path, owner)
159
134
 
160
135
    # usage: tramp uid jail_dir working_dir script_path
161
 
    pid = subprocess.Popen(
162
 
        [trampoline, str(uid), jail_dir, working_dir, interpreter,
163
 
        script_path],
 
136
    cmd_line = [trampoline, str(owner.unixid),
 
137
            req.config['paths']['jails']['mounts'],
 
138
            req.config['paths']['jails']['src'],
 
139
            req.config['paths']['jails']['template'],
 
140
            jail_dir, working_dir, interpreter, script_path]
 
141
    # Popen doesn't like unicode strings. It hateses them.
 
142
    cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
 
143
                for s in cmd_line]
 
144
    pid = subprocess.Popen(cmd_line,
164
145
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
165
 
        cwd=tramp_dir)
166
 
 
167
 
    # Restore the environment
168
 
    for k in os.environ.keys():
169
 
        del os.environ[k]
170
 
    for (k,v) in old_env.items():
171
 
        os.environ[k] = v
 
146
        cwd=tramp_dir, env=environ)
172
147
 
173
148
    # We don't want any output! Bail out after the process terminates.
174
149
    if noop:
240
215
            if len(split) == 1:
241
216
                split = headers.split('\n', 1)
242
217
 
243
 
        # Is this an internal IVLE error condition?
244
 
        hs = cgiflags.headers
245
 
        if 'X-IVLE-Error-Type' in hs:
246
 
            t = hs['X-IVLE-Error-Type']
247
 
            if t == IVLEError.__name__:
248
 
                raise IVLEError(int(hs['X-IVLE-Error-Code']),
249
 
                                hs['X-IVLE-Error-Message'])
250
 
            else:
 
218
        # If not executing in gentle mode (which presents CGI violations
 
219
        # to users nicely), check if this an internal IVLE error
 
220
        # condition.
 
221
        if not cgiflags.gentle:
 
222
            hs = cgiflags.headers
 
223
            if 'X-IVLE-Error-Type' in hs:
251
224
                try:
252
225
                    raise IVLEJailError(hs['X-IVLE-Error-Type'],
253
226
                                        hs['X-IVLE-Error-Message'],
254
227
                                        hs['X-IVLE-Error-Info'])
255
228
                except KeyError:
256
 
                    raise IVLEError(500, 'bad error headers written by CGI')
 
229
                    raise AssertionError("Bad error headers written by CGI.")
257
230
 
258
231
        # Check to make sure the required headers were written
259
232
        if cgiflags.wrote_html_warning or not cgiflags.gentle:
362
335
    <pre>
363
336
""" % (warning, text))
364
337
 
365
 
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
366
 
 
367
338
# Mapping of interpreter names (as given in conf/app/server.py) to
368
339
# interpreter functions.
369
340
 
370
341
interpreter_objects = {
371
342
    'cgi-python'
372
 
        : functools.partial(execute_cgi, "/usr/bin/python",
373
 
            location_cgi_python),
 
343
        : functools.partial(execute_cgi, "/usr/bin/python"),
374
344
    'noop'
375
 
        : functools.partial(execute_cgi, None,
376
 
            location_cgi_python),
 
345
        : functools.partial(execute_cgi, None),
377
346
    # Should also have:
378
347
    # cgi-generic
379
348
    # python-server-page
380
349
}
381
350
 
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.
 
351
def cgi_environ(req, script_path, user):
 
352
    """Gets CGI variables from apache and makes a few changes for security and 
 
353
    correctness.
385
354
 
386
355
    Does not modify req, only reads it.
387
356
    """
388
 
    env = os.environ
 
357
    env = {}
389
358
    # Comments here are on the heavy side, explained carefully for security
390
359
    # reasons. Please read carefully before making changes.
 
360
    
 
361
    # This automatically asks mod_python to load up the CGI variables into the
 
362
    # environment (which is a good first approximation)
 
363
    for (k,v) in req.get_cgi_environ().items():
 
364
        env[k] = v
391
365
 
392
366
    # Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
393
367
    # exposes unnecessary details about server.
405
379
        del env['PATH']
406
380
    except: pass
407
381
 
408
 
    # Remove SCRIPT_FILENAME. Not part of CGI spec (see SCRIPT_NAME).
409
 
 
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'] = ''
416
 
 
417
382
    # CGI specifies that REMOTE_HOST SHOULD be set, and MAY just be set to
418
383
    # REMOTE_ADDR. Since Apache does not appear to set this, set it to
419
384
    # REMOTE_ADDR.
420
385
    if 'REMOTE_HOST' not in env and 'REMOTE_ADDR' in env:
421
386
        env['REMOTE_HOST'] = env['REMOTE_ADDR']
422
387
 
 
388
    env['PATH_INFO'] = ''
 
389
    del env['PATH_TRANSLATED']
 
390
 
 
391
    normuri = os.path.normpath(req.uri)
 
392
    env['SCRIPT_NAME'] = normuri
 
393
 
423
394
    # SCRIPT_NAME is the path to the script WITHOUT PATH_INFO.
424
 
    script_name = req.uri
425
 
    env['SCRIPT_NAME'] = script_name
 
395
    # We don't care about these if the script is null (ie. noop).
 
396
    # XXX: We check for /home because we don't want to interfere with
 
397
    # CGIRequest, which fileservice still uses.
 
398
    if script_path and script_path.startswith('/home'):
 
399
        normscript = os.path.normpath(script_path)
 
400
 
 
401
        uri_into_jail = studpath.to_home_path(os.path.normpath(req.path))
 
402
 
 
403
        # PATH_INFO is wrong because the script doesn't physically exist.
 
404
        env['PATH_INFO'] = uri_into_jail[len(normscript):]
 
405
        if len(env['PATH_INFO']) > 0:
 
406
            env['SCRIPT_NAME'] = normuri[:-len(env['PATH_INFO'])]
426
407
 
427
408
    # SERVER_SOFTWARE is actually not Apache but IVLE, since we are
428
409
    # custom-making the CGI request.
429
 
    env['SERVER_SOFTWARE'] = "IVLE/" + str(ivle.conf.ivle_version)
 
410
    env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
430
411
 
431
412
    # Additional environment variables
432
 
    username = studpath.url_to_jailpaths(req.path)[0]
 
413
    username = user.login
433
414
    env['HOME'] = os.path.join('/home', username)
 
415
 
 
416
    return env
 
417
 
 
418
class ExecutionError(Exception):
 
419
    pass
 
420
 
 
421
def execute_raw(config, user, jail_dir, working_dir, binary, args):
 
422
    '''Execute a binary in a user's jail, returning the raw output.
 
423
 
 
424
    The binary is executed in the given working directory with the given
 
425
    args. A tuple of (stdout, stderr) is returned.
 
426
    '''
 
427
 
 
428
    tramp = os.path.join(config['paths']['lib'], 'trampoline')
 
429
    tramp_dir = os.path.split(tramp)[0]
 
430
 
 
431
    # Fire up trampoline. Vroom, vroom.
 
432
    cmd_line = [tramp, str(user.unixid), config['paths']['jails']['mounts'],
 
433
         config['paths']['jails']['src'],
 
434
         config['paths']['jails']['template'],
 
435
         jail_dir, working_dir, binary] + args
 
436
    # Popen doesn't like unicode strings. It hateses them.
 
437
    cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
 
438
                for s in cmd_line]
 
439
    proc = subprocess.Popen(cmd_line,
 
440
        stdin=subprocess.PIPE, stdout=subprocess.PIPE,
 
441
        stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True,
 
442
        env={'HOME': os.path.join('/home', user.login),
 
443
             'PATH': PATH,
 
444
             'USER': user.login,
 
445
             'LOGNAME': user.login})
 
446
 
 
447
    (stdout, stderr) = proc.communicate()
 
448
    exitcode = proc.returncode
 
449
 
 
450
    if exitcode != 0:
 
451
        raise ExecutionError('subprocess ended with code %d, stderr: "%s"' %
 
452
                             (exitcode, stderr))
 
453
    return (stdout, stderr)