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

« back to all changes in this revision

Viewing changes to ivle/interpret.py

  • Committer: William Grant
  • Date: 2009-06-24 10:47:38 UTC
  • mfrom: (1288 trunk)
  • mto: (1281.1.8 aufsless)
  • mto: This revision was merged to the branch mainline in revision 1300.
  • Revision ID: grantw@unimelb.edu.au-20090624104738-ezgfy17mjzwz09i3
MergeĀ fromĀ trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
 
24
24
import ivle
25
25
from ivle import studpath
26
 
from ivle.util import IVLEJailError, split_path
 
26
from ivle.util import IVLEError, IVLEJailError, split_path
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, uid, jail_dir, working_dir, script_path,
 
92
                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
138
129
        f.seek(0)       # Rewind, for reading
139
130
 
140
131
    # Set up the environment
141
 
    environ = cgi_environ(req, script_path, owner, overrides=overrides)
 
132
    # This automatically asks mod_python to load up the CGI variables into the
 
133
    # environment (which is a good first approximation)
 
134
    old_env = os.environ.copy()
 
135
    for k in os.environ.keys():
 
136
        del os.environ[k]
 
137
    for (k,v) in req.get_cgi_environ().items():
 
138
        os.environ[k] = v
 
139
    fixup_environ(req, script_path)
142
140
 
143
141
    # 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,
 
142
    pid = subprocess.Popen(
 
143
        [trampoline, str(uid), req.config['paths']['jails']['mounts'],
 
144
         req.config['paths']['jails']['src'],
 
145
         req.config['paths']['jails']['template'],
 
146
         jail_dir, working_dir, interpreter, script_path],
153
147
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
154
 
        cwd=tramp_dir, env=environ)
 
148
        cwd=tramp_dir)
 
149
 
 
150
    # Restore the environment
 
151
    for k in os.environ.keys():
 
152
        del os.environ[k]
 
153
    for (k,v) in old_env.items():
 
154
        os.environ[k] = v
155
155
 
156
156
    # We don't want any output! Bail out after the process terminates.
157
157
    if noop:
223
223
            if len(split) == 1:
224
224
                split = headers.split('\n', 1)
225
225
 
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:
 
226
        # Is this an internal IVLE error condition?
 
227
        hs = cgiflags.headers
 
228
        if 'X-IVLE-Error-Type' in hs:
 
229
            t = hs['X-IVLE-Error-Type']
 
230
            if t == IVLEError.__name__:
 
231
                raise IVLEError(int(hs['X-IVLE-Error-Code']),
 
232
                                hs['X-IVLE-Error-Message'])
 
233
            else:
232
234
                try:
233
235
                    raise IVLEJailError(hs['X-IVLE-Error-Type'],
234
236
                                        hs['X-IVLE-Error-Message'],
235
237
                                        hs['X-IVLE-Error-Info'])
236
238
                except KeyError:
237
 
                    raise AssertionError("Bad error headers written by CGI.")
 
239
                    raise IVLEError(500, 'bad error headers written by CGI')
238
240
 
239
241
        # Check to make sure the required headers were written
240
242
        if cgiflags.wrote_html_warning or not cgiflags.gentle:
294
296
        process_cgi_output(req, line + '\n', cgiflags)
295
297
        return
296
298
 
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
299
    # Read CGI headers
318
300
    value = value.strip()
319
301
    if name == "Content-Type":
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, script_path):
 
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.
441
415
    env['SERVER_SOFTWARE'] = "IVLE/" + ivle.__version__
442
416
 
443
417
    # Additional environment variables
444
 
    username = user.login
 
418
    username = split_path(req.path)[0]
445
419
    env['HOME'] = os.path.join('/home', username)
446
420
 
447
 
    if overrides is not None:
448
 
        env.update(overrides)
449
 
    return env
450
 
 
451
421
class ExecutionError(Exception):
452
422
    pass
453
423
 
462
432
    tramp_dir = os.path.split(tramp)[0]
463
433
 
464
434
    # Fire up trampoline. Vroom, vroom.
465
 
    cmd_line = [tramp, str(user.unixid), config['paths']['jails']['mounts'],
 
435
    proc = subprocess.Popen(
 
436
        [tramp, str(user.unixid), config['paths']['jails']['mounts'],
466
437
         config['paths']['jails']['src'],
467
438
         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,
 
439
         jail_dir, 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})
 
441
        stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True)
479
442
 
480
443
    (stdout, stderr) = proc.communicate()
481
444
    exitcode = proc.returncode
482
445
 
483
446
    if exitcode != 0:
484
 
        raise ExecutionError('subprocess ended with code %d, stderr: "%s"' %
485
 
                             (exitcode, stderr))
 
447
        raise ExecutionError('subprocess ended with code %d, stderr %s' %
 
448
                             (exitcode, proc.stderr.read()))
486
449
    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)