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

« back to all changes in this revision

Viewing changes to www/common/interpret.py

  • Committer: drtomc
  • Date: 2007-12-21 00:20:24 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:111
Checkpoint work on the console.

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: 18/1/2008
 
20
# Date: 20/12/2007
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.
29
23
 
30
24
from common import studpath
31
25
import conf
34
28
import os
35
29
import pwd
36
30
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
44
31
 
45
32
def interpret_file(req, owner, filename, interpreter):
46
33
    """Serves a file by interpreting it using one of IVLE's builtin
84
71
 
85
72
    return interpreter(uid, jail_dir, working_dir, path, req)
86
73
 
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
 
74
# Used to store mutable data
 
75
class Dummy:
 
76
    pass
95
77
 
96
78
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
97
79
                script_path, req):
123
105
        f.flush()
124
106
        f.seek(0)       # Rewind, for reading
125
107
 
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
 
 
136
108
    # usage: tramp uid jail_dir working_dir script_path
137
109
    pid = subprocess.Popen(
138
110
        [trampoline, str(uid), jail_dir, working_dir, interpreter,
140
112
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
141
113
        cwd=tramp_dir)
142
114
 
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
 
 
149
115
    # process_cgi_line: Reads a single line of CGI output and processes it.
150
116
    # Prints to req, and also does fancy HTML warnings if Content-Type
151
117
    # omitted.
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))
 
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)
178
127
        else:
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):
 
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")
222
144
                pass
223
145
            else:
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"
 
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"
295
152
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
296
153
<html xmlns="http://www.w3.org/1999/xhtml">
297
154
<head>
298
155
  <meta http-equiv="Content-Type"
299
156
    content="text/html; charset=utf-8" />
300
157
</head>
301
 
<body style="margin: 0; padding: 0; font-family: sans-serif;">
 
158
<body style="margin: 0; padding: 0; font-family: sans;">
302
159
  <div style="background-color: #faa; border-bottom: 1px solid black;
303
160
    padding: 8px;">
304
 
    <p><strong>Warning</strong>: %s
 
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>
305
164
  </div>
306
165
  <div style="margin: 8px;">
307
166
    <pre>
308
 
""" % text)
309
 
 
 
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
310
185
location_cgi_python = os.path.join(conf.ivle_install_dir,
311
186
    "bin/trampoline")
312
187
 
322
197
    # python-server-page
323
198
}
324
199
 
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)