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

« back to all changes in this revision

Viewing changes to ivle/interpret.py

  • Committer: me at id
  • Date: 2009-01-15 01:29:07 UTC
  • mto: This revision was merged to the branch mainline in revision 1090.
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:branches%2Fstorm:1147
ivle.makeuser: Don't create a local.auth. It wasn't used anywhere, and seems
    to have only been there for historical reasons.
setup.configure: Remove svn_auth_local from the config.py boilerplate; it was
    used solely by ivle.makeuser.

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 import db
 
26
from ivle.util import IVLEError, IVLEJailError
 
27
import ivle.conf
27
28
 
28
29
import functools
29
30
 
31
32
import pwd
32
33
import subprocess
33
34
import cgi
34
 
import StringIO
35
35
 
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
39
39
 
40
40
CGI_BLOCK_SIZE = 65535
41
 
PATH = "/usr/local/bin:/usr/bin:/bin"
42
 
 
43
 
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True,
44
 
    overrides=None):
 
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]
 
59
 
 
60
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
45
61
    """Serves a file by interpreting it using one of IVLE's builtin
46
62
    interpreters. All interpreters are intended to run in the user's jail. The
47
63
    jail location is provided as an argument to the interpreter but it is up
48
64
    to the individual interpreters to create the jail.
49
65
 
50
66
    req: An IVLE request object.
51
 
    owner: The user who owns the file being served.
 
67
    owner: Username of the user who owns the file being served.
52
68
    jail_dir: Absolute path to the user's jail.
53
69
    filename: Absolute filename within the user's jail.
54
70
    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
71
    """
59
72
    # We can't test here whether or not the target file actually exists,
60
73
    # because the apache user may not have permission. Instead we have to
66
79
        filename_abs = os.path.join(os.sep, filename)
67
80
        filename_rel = filename
68
81
 
 
82
    # Get the UID of the owner of the file
69
83
    # (Note: files are executed by their owners, not the logged in user.
70
84
    # This ensures users are responsible for their own programs and also
71
85
    # allows them to be executed by the public).
 
86
    uid = get_uid(owner)
72
87
 
73
88
    # Split up req.path again, this time with respect to the jail
74
89
    (working_dir, _) = os.path.split(filename_abs)
79
94
    # (Note that paths "relative" to the jail actually begin with a '/' as
80
95
    # they are absolute in the jailspace)
81
96
 
82
 
    return interpreter(owner, jail_dir, working_dir, filename_abs, req,
83
 
                       gentle, overrides=overrides)
 
97
    return interpreter(uid, jail_dir, working_dir, filename_abs, req,
 
98
                       gentle)
84
99
 
85
100
class CGIFlags:
86
101
    """Stores flags regarding the state of reading CGI output.
94
109
        self.linebuf = ""
95
110
        self.headers = {}       # Header names : values
96
111
 
97
 
def execute_cgi(interpreter, owner, jail_dir, working_dir, script_path,
98
 
                req, gentle, overrides=None):
 
112
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
 
113
                script_path, req, gentle):
99
114
    """
100
115
    trampoline: Full path on the local system to the CGI wrapper program
101
116
        being executed.
102
 
    owner: User object of the owner of the file.
 
117
    uid: User ID of the owner of the file.
103
118
    jail_dir: Absolute path of owner's jail directory.
104
119
    working_dir: Directory containing the script file relative to owner's
105
120
        jail.
106
121
    script_path: CGI script relative to the owner's jail.
107
122
    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
123
 
112
124
    The called CGI wrapper application shall be called using popen and receive
113
125
    the HTTP body on stdin. It shall receive the CGI environment variables to
114
126
    its environment.
115
127
    """
116
128
 
117
 
    trampoline = os.path.join(req.config['paths']['lib'], 'trampoline')
118
 
 
119
129
    # Support no-op trampoline runs.
120
130
    if interpreter is None:
121
131
        interpreter = '/bin/true'
138
148
        f.seek(0)       # Rewind, for reading
139
149
 
140
150
    # Set up the environment
141
 
    environ = cgi_environ(req, script_path, owner, overrides=overrides)
 
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)
142
159
 
143
160
    # 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,
 
161
    pid = subprocess.Popen(
 
162
        [trampoline, str(uid), jail_dir, working_dir, interpreter,
 
163
        script_path],
153
164
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
154
 
        cwd=tramp_dir, env=environ)
 
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
155
172
 
156
173
    # We don't want any output! Bail out after the process terminates.
157
174
    if noop:
223
240
            if len(split) == 1:
224
241
                split = headers.split('\n', 1)
225
242
 
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:
 
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:
232
251
                try:
233
252
                    raise IVLEJailError(hs['X-IVLE-Error-Type'],
234
253
                                        hs['X-IVLE-Error-Message'],
235
254
                                        hs['X-IVLE-Error-Info'])
236
255
                except KeyError:
237
 
                    raise AssertionError("Bad error headers written by CGI.")
 
256
                    raise IVLEError(500, 'bad error headers written by CGI')
238
257
 
239
258
        # Check to make sure the required headers were written
240
259
        if cgiflags.wrote_html_warning or not cgiflags.gentle:
294
313
        process_cgi_output(req, line + '\n', cgiflags)
295
314
        return
296
315
 
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
316
    # Read CGI headers
318
317
    value = value.strip()
319
318
    if name == "Content-Type":
363
362
    <pre>
364
363
""" % (warning, text))
365
364
 
 
365
location_cgi_python = os.path.join(ivle.conf.lib_path, "trampoline")
 
366
 
366
367
# Mapping of interpreter names (as given in conf/app/server.py) to
367
368
# interpreter functions.
368
369
 
369
370
interpreter_objects = {
370
371
    'cgi-python'
371
 
        : functools.partial(execute_cgi, "/usr/bin/python"),
 
372
        : functools.partial(execute_cgi, "/usr/bin/python",
 
373
            location_cgi_python),
372
374
    'noop'
373
 
        : functools.partial(execute_cgi, None),
 
375
        : functools.partial(execute_cgi, None,
 
376
            location_cgi_python),
374
377
    # Should also have:
375
378
    # cgi-generic
376
379
    # python-server-page
377
380
}
378
381
 
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.
 
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.
382
385
 
383
386
    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
387
    """
388
 
    env = {}
 
388
    env = os.environ
389
389
    # Comments here are on the heavy side, explained carefully for security
390
390
    # 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
391
 
397
392
    # Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
398
393
    # exposes unnecessary details about server.
410
405
        del env['PATH']
411
406
    except: pass
412
407
 
 
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
 
413
417
    # CGI specifies that REMOTE_HOST SHOULD be set, and MAY just be set to
414
418
    # REMOTE_ADDR. Since Apache does not appear to set this, set it to
415
419
    # REMOTE_ADDR.
416
420
    if 'REMOTE_HOST' not in env and 'REMOTE_ADDR' in env:
417
421
        env['REMOTE_HOST'] = env['REMOTE_ADDR']
418
422
 
419
 
    env['PATH_INFO'] = ''
420
 
    del env['PATH_TRANSLATED']
421
 
 
422
 
    normuri = os.path.normpath(req.uri)
423
 
    env['SCRIPT_NAME'] = normuri
424
 
 
425
423
    # 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'])]
 
424
    script_name = req.uri
 
425
    env['SCRIPT_NAME'] = script_name
438
426
 
439
427
    # SERVER_SOFTWARE is actually not Apache but IVLE, since we are
440
428
    # custom-making the CGI request.
441
 
    env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
 
429
    env['SERVER_SOFTWARE'] = "IVLE/" + str(ivle.conf.ivle_version)
442
430
 
443
431
    # Additional environment variables
444
 
    username = user.login
 
432
    username = studpath.url_to_jailpaths(req.path)[0]
445
433
    env['HOME'] = os.path.join('/home', username)
446
 
 
447
 
    if overrides is not None:
448
 
        env.update(overrides)
449
 
    return env
450
 
 
451
 
class ExecutionError(Exception):
452
 
    pass
453
 
 
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.
456
 
 
457
 
    The binary is executed in the given working directory with the given
458
 
    args. A tuple of (stdout, stderr) is returned.
459
 
    '''
460
 
 
461
 
    tramp = os.path.join(config['paths']['lib'], 'trampoline')
462
 
    tramp_dir = os.path.split(tramp)[0]
463
 
 
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)
471
 
                for s in cmd_line]
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),
476
 
             'PATH': PATH,
477
 
             'USER': user.login,
478
 
             'LOGNAME': user.login})
479
 
 
480
 
    (stdout, stderr) = proc.communicate()
481
 
    exitcode = proc.returncode
482
 
 
483
 
    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)