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

« back to all changes in this revision

Viewing changes to www/common/interpret.py

  • Committer: stevenbird
  • Date: 2008-02-01 03:51:56 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:368
First version of a DTD for XML problem files

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
# Module: Interpret
19
19
# Author: Matt Giuca
20
 
# Date: 20/12/2007
 
20
# Date: 18/1/2008
21
21
 
22
22
# Runs a student script in a safe execution environment.
 
23
#
 
24
# NOTE: This script currently disables cookies. This means students will be
 
25
# unable to write session-based or stateful web applications. This is done for
 
26
# security reasons (we do not want the students to see the IVLE cookie of
 
27
# whoever is visiting their pages).
 
28
# This can be resolved but needs careful sanitisation. See fixup_environ.
23
29
 
24
30
from common import studpath
25
31
import conf
28
34
import os
29
35
import pwd
30
36
import subprocess
 
37
import cgi
 
38
 
 
39
# TODO: Make progressive output work
 
40
# Question: Will having a large buffer size stop progressive output from
 
41
# working on smaller output
 
42
 
 
43
CGI_BLOCK_SIZE = 65535
31
44
 
32
45
def interpret_file(req, owner, filename, interpreter):
33
46
    """Serves a file by interpreting it using one of IVLE's builtin
71
84
 
72
85
    return interpreter(uid, jail_dir, working_dir, path, req)
73
86
 
74
 
# Used to store mutable data
75
 
class Dummy:
76
 
    pass
 
87
class CGIFlags:
 
88
    """Stores flags regarding the state of reading CGI output."""
 
89
    def __init__(self):
 
90
        self.started_cgi_body = False
 
91
        self.got_cgi_headers = False
 
92
        self.wrote_html_warning = False
 
93
        self.linebuf = ""
 
94
        self.headers = {}       # Header names : values
77
95
 
78
96
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
79
97
                script_path, req):
105
123
        f.flush()
106
124
        f.seek(0)       # Rewind, for reading
107
125
 
 
126
    # Set up the environment
 
127
    # This automatically asks mod_python to load up the CGI variables into the
 
128
    # environment (which is a good first approximation)
 
129
    old_env = os.environ.copy()
 
130
    for k in os.environ.keys():
 
131
        del os.environ[k]
 
132
    for (k,v) in req.get_cgi_environ().items():
 
133
        os.environ[k] = v
 
134
    fixup_environ(req)
 
135
 
108
136
    # usage: tramp uid jail_dir working_dir script_path
109
137
    pid = subprocess.Popen(
110
138
        [trampoline, str(uid), jail_dir, working_dir, interpreter,
112
140
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
113
141
        cwd=tramp_dir)
114
142
 
 
143
    # Restore the environment
 
144
    for k in os.environ.keys():
 
145
        del os.environ[k]
 
146
    for (k,v) in old_env.items():
 
147
        os.environ[k] = v
 
148
 
115
149
    # process_cgi_line: Reads a single line of CGI output and processes it.
116
150
    # Prints to req, and also does fancy HTML warnings if Content-Type
117
151
    # omitted.
118
 
    cgiflags = Dummy()
119
 
    cgiflags.got_cgi_header = False
120
 
    cgiflags.started_cgi_body = False
121
 
    cgiflags.wrote_html_warning = False
122
 
    def process_cgi_line(line):
123
 
        # FIXME? Issue with binary files (processing per-line?)
124
 
        if cgiflags.started_cgi_body:
125
 
            # FIXME: HTML escape text if wrote_html_warning
126
 
            req.write(line)
 
152
    cgiflags = CGIFlags()
 
153
 
 
154
    # Read from the process's stdout into req
 
155
    data = pid.stdout.read(CGI_BLOCK_SIZE)
 
156
    while len(data) > 0:
 
157
        process_cgi_output(req, data, cgiflags)
 
158
        data = pid.stdout.read(CGI_BLOCK_SIZE)
 
159
 
 
160
    # If we haven't processed headers yet, now is a good time
 
161
    if not cgiflags.started_cgi_body:
 
162
        process_cgi_output(req, '\n', cgiflags)
 
163
 
 
164
    # If we wrote an HTML warning header, write the footer
 
165
    if cgiflags.wrote_html_warning:
 
166
        req.write("""</pre>
 
167
  </div>
 
168
</body>
 
169
</html>""")
 
170
 
 
171
def process_cgi_output(req, data, cgiflags):
 
172
    """Processes a chunk of CGI output. data is a string of arbitrary length;
 
173
    some arbitrary chunk of output written by the CGI script."""
 
174
    if cgiflags.started_cgi_body:
 
175
        if cgiflags.wrote_html_warning:
 
176
            # HTML escape text if wrote_html_warning
 
177
            req.write(cgi.escape(data))
127
178
        else:
128
 
            # Read CGI headers
129
 
            if line.strip() == "" and cgiflags.got_cgi_header:
130
 
                cgiflags.started_cgi_body = True
131
 
            elif line.startswith("Content-Type:"):
132
 
                req.content_type = line[13:].strip()
133
 
                cgiflags.got_cgi_header = True
134
 
            elif line.startswith("Location:"):
135
 
                # TODO
136
 
                cgiflags.got_cgi_header = True
137
 
            elif line.startswith("Status:"):
138
 
                # TODO
139
 
                cgiflags.got_cgi_header = True
140
 
            elif cgiflags.got_cgi_header:
141
 
                # Invalid header
142
 
                # TODO
143
 
                req.write("Invalid header")
 
179
            req.write(data)
 
180
    else:
 
181
        # Break data into lines of CGI header data. 
 
182
        linebuf = cgiflags.linebuf + data
 
183
        # First see if we can split all header data
 
184
        split = linebuf.split('\r\n\r\n', 1)
 
185
        if len(split) == 1:
 
186
            # Allow UNIX newlines instead
 
187
            split = linebuf.split('\n\n', 1)
 
188
        if len(split) == 1:
 
189
            # Haven't seen all headers yet. Buffer and come back later.
 
190
            cgiflags.linebuf = linebuf
 
191
            return
 
192
 
 
193
        headers = split[0]
 
194
        data = split[1]
 
195
        cgiflags.linebuf = ""
 
196
        cgiflags.started_cgi_body = True
 
197
        # Process all the header lines
 
198
        split = headers.split('\r\n', 1)
 
199
        if len(split) == 1:
 
200
            split = headers.split('\n', 1)
 
201
        while True:
 
202
            process_cgi_header_line(req, split[0], cgiflags)
 
203
            if len(split) == 1: break
 
204
            headers = split[1]
 
205
            if cgiflags.wrote_html_warning:
 
206
                # We're done with headers. Treat the rest as data.
 
207
                data = headers + '\n' + data
 
208
                break
 
209
            split = headers.split('\r\n', 1)
 
210
            if len(split) == 1:
 
211
                split = headers.split('\n', 1)
 
212
 
 
213
        # Check to make sure the required headers were written
 
214
        if cgiflags.wrote_html_warning:
 
215
            # We already reported an error, that's enough
 
216
            pass
 
217
        elif "Content-Type" in cgiflags.headers:
 
218
            pass
 
219
        elif "Location" in cgiflags.headers:
 
220
            if ("Status" in cgiflags.headers and req.status >= 300
 
221
                and req.status < 400):
144
222
                pass
145
223
            else:
146
 
                # Assume the user is not printing headers and give a warning
147
 
                # about that.
148
 
                # User program did not print header.
149
 
                # Make a fancy HTML warning for them.
150
 
                req.content_type = "text/html"
151
 
                req.write("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 
224
                message = """You did not write a valid status code for
 
225
the given location. To make a redirect, you may wish to try:</p>
 
226
<pre style="margin-left: 1em">Status: 302 Found
 
227
Location: &lt;redirect address&gt;</pre>"""
 
228
                write_html_warning(req, message)
 
229
                cgiflags.wrote_html_warning = True
 
230
        else:
 
231
            message = """You did not print a Content-Type header.
 
232
CGI requires that you print a "Content-Type". You may wish to try:</p>
 
233
<pre style="margin-left: 1em">Content-Type: text/html</pre>"""
 
234
            write_html_warning(req, message)
 
235
            cgiflags.wrote_html_warning = True
 
236
 
 
237
        # Call myself to flush out the extra bit of data we read
 
238
        process_cgi_output(req, data, cgiflags)
 
239
 
 
240
def process_cgi_header_line(req, line, cgiflags):
 
241
    """Process a line of CGI header data. line is a string representing a
 
242
    complete line of text, stripped and without the newline.
 
243
    """
 
244
    try:
 
245
        name, value = line.split(':', 1)
 
246
    except ValueError:
 
247
        # No colon. The user did not write valid headers.
 
248
        if len(cgiflags.headers) == 0:
 
249
            # First line was not a header line. We can assume this is not
 
250
            # a CGI app.
 
251
            message = """You did not print a CGI header.
 
252
CGI requires that you print a "Content-Type". You may wish to try:</p>
 
253
<pre style="margin-left: 1em">Content-Type: text/html</pre>"""
 
254
        else:
 
255
            # They printed some header at least, but there was an invalid
 
256
            # header.
 
257
            message = """You printed an invalid CGI header. You need to leave
 
258
a blank line after the headers, before writing the page contents."""
 
259
        write_html_warning(req, message)
 
260
        cgiflags.wrote_html_warning = True
 
261
        # Handle the rest of this line as normal data
 
262
        process_cgi_output(req, line + '\n', cgiflags)
 
263
        return
 
264
 
 
265
    # Read CGI headers
 
266
    value = value.strip()
 
267
    if name == "Content-Type":
 
268
        req.content_type = value
 
269
    elif name == "Location":
 
270
        req.location = value
 
271
    elif name == "Status":
 
272
        # Must be an integer, followed by a space, and then the status line
 
273
        # which we ignore (seems like Apache has no way to send a custom
 
274
        # status line).
 
275
        try:
 
276
            req.status = int(value.split(' ', 1)[0])
 
277
        except ValueError:
 
278
            message = """The "Status" CGI header was invalid. You need to
 
279
print a number followed by a message, such as "302 Found"."""
 
280
            write_html_warning(req, message)
 
281
            cgiflags.wrote_html_warning = True
 
282
            # Handle the rest of this line as normal data
 
283
            process_cgi_output(req, line + '\n', cgiflags)
 
284
    else:
 
285
        # Generic HTTP header
 
286
        # FIXME: Security risk letting users write arbitrary headers?
 
287
        req.headers_out[name] = value
 
288
    cgiflags.headers[name] = value
 
289
 
 
290
def write_html_warning(req, text):
 
291
    """Prints an HTML warning about invalid CGI interaction on the part of the
 
292
    user. text may contain HTML markup."""
 
293
    req.content_type = "text/html"
 
294
    req.write("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
152
295
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
153
296
<html xmlns="http://www.w3.org/1999/xhtml">
154
297
<head>
155
298
  <meta http-equiv="Content-Type"
156
299
    content="text/html; charset=utf-8" />
157
300
</head>
158
 
<body style="margin: 0; padding: 0; font-family: sans;">
 
301
<body style="margin: 0; padding: 0; font-family: sans-serif;">
159
302
  <div style="background-color: #faa; border-bottom: 1px solid black;
160
303
    padding: 8px;">
161
 
    <p><strong>Warning</strong>: You did not print a "Content-Type" header.
162
 
    CGI requires you to print some content type. You may wish to try:</p>
163
 
    <pre style="margin-left: 1em">Content-Type: text/html</pre>
 
304
    <p><strong>Warning</strong>: %s
164
305
  </div>
165
306
  <div style="margin: 8px;">
166
307
    <pre>
167
 
""")
168
 
                cgiflags.got_cgi_header = True
169
 
                cgiflags.wrote_html_warning = True
170
 
                cgiflags.started_cgi_body = True
171
 
                req.write(line)
172
 
 
173
 
    # Read from the process's stdout into req
174
 
    for line in pid.stdout:
175
 
        process_cgi_line(line)
176
 
 
177
 
    # If we wrote an HTML warning header, write the footer
178
 
    if cgiflags.wrote_html_warning:
179
 
        req.write("""</pre>
180
 
  </div>
181
 
</body>
182
 
</html>""")
183
 
 
184
 
# TODO: Replace mytest with cgi trampoline handler script
 
308
""" % text)
 
309
 
185
310
location_cgi_python = os.path.join(conf.ivle_install_dir,
186
311
    "bin/trampoline")
187
312
 
197
322
    # python-server-page
198
323
}
199
324
 
 
325
def fixup_environ(req):
 
326
    """Assuming os.environ has been written with the CGI variables from
 
327
    apache, make a few changes for security and correctness.
 
328
 
 
329
    Does not modify req, only reads it.
 
330
    """
 
331
    env = os.environ
 
332
    # Comments here are on the heavy side, explained carefully for security
 
333
    # reasons. Please read carefully before making changes.
 
334
 
 
335
    # Remove HTTP_COOKIE. It is a security risk to have students see the IVLE
 
336
    # cookie of their visitors.
 
337
    try:
 
338
        del env['HTTP_COOKIE']
 
339
    except: pass
 
340
 
 
341
    # Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
 
342
    # exposes unnecessary details about server.
 
343
    try:
 
344
        del env['DOCUMENT_ROOT']
 
345
    except: pass
 
346
    try:
 
347
        del env['SCRIPT_FILENAME']
 
348
    except: pass
 
349
 
 
350
    # Remove PATH. The PATH here is the path on the server machine; not useful
 
351
    # inside the jail. It may be a good idea to add another path, reflecting
 
352
    # the inside of the jail, but not done at this stage.
 
353
    try:
 
354
        del env['PATH']
 
355
    except: pass
 
356
 
 
357
    # Remove SCRIPT_FILENAME. Not part of CGI spec (see SCRIPT_NAME).
 
358
 
 
359
    # PATH_INFO is wrong because the script doesn't physically exist.
 
360
    # Apache makes it relative to the "serve" app. It should actually be made
 
361
    # relative to the student's script.
 
362
    # TODO: At this stage, it is not possible to add a path after the script,
 
363
    # so PATH_INFO is always "".
 
364
    path_info = ""
 
365
    env['PATH_INFO'] = path_info
 
366
 
 
367
    # PATH_TRANSLATED currently points to a non-existant location within the
 
368
    # local web server directory. Instead make it represent a path within the
 
369
    # student jail.
 
370
    (username, _, path_translated) = studpath.url_to_jailpaths(req.path)
 
371
    if len(path_translated) == 0 or path_translated[0] != os.sep:
 
372
        path_translated = os.sep + path_translated
 
373
    env['PATH_TRANSLATED'] = path_translated
 
374
 
 
375
    # CGI specifies that REMOTE_HOST SHOULD be set, and MAY just be set to
 
376
    # REMOTE_ADDR. Since Apache does not appear to set this, set it to
 
377
    # REMOTE_ADDR.
 
378
    if 'REMOTE_HOST' not in env and 'REMOTE_ADDR' in env:
 
379
        env['REMOTE_HOST'] = env['REMOTE_ADDR']
 
380
 
 
381
    # SCRIPT_NAME is the path to the script WITHOUT PATH_INFO.
 
382
    script_name = req.uri
 
383
    if len(path_info) > 0:
 
384
        script_name = script_name[:-len(path_info)]
 
385
    env['SCRIPT_NAME'] = script_name
 
386
 
 
387
    # SERVER_SOFTWARE is actually not Apache but IVLE, since we are
 
388
    # custom-making the CGI request.
 
389
    env['SERVER_SOFTWARE'] = "IVLE/" + str(conf.ivle_version)
 
390
 
 
391
    # Additional environment variables
 
392
    env['HOME'] = os.path.join('/home', username)