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

« back to all changes in this revision

Viewing changes to www/common/interpret.py

  • Committer: mattgiuca
  • Date: 2008-01-18 05:03:08 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:246
request.py: Fixed Request.read (wasn't returning anything).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE
 
2
# Copyright (C) 2007-2008 The University of Melbourne
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
17
 
 
18
# Module: Interpret
 
19
# Author: Matt Giuca
 
20
# Date: 18/1/2008
 
21
 
 
22
# Runs a student script in a safe execution environment.
 
23
 
 
24
from common import studpath
 
25
import conf
 
26
import functools
 
27
 
 
28
import os
 
29
import pwd
 
30
import subprocess
 
31
import cgi
 
32
 
 
33
# TODO: Make progressive output work
 
34
# Question: Will having a large buffer size stop progressive output from
 
35
# working on smaller output
 
36
 
 
37
CGI_BLOCK_SIZE = 65535
 
38
 
 
39
def interpret_file(req, owner, filename, interpreter):
 
40
    """Serves a file by interpreting it using one of IVLE's builtin
 
41
    interpreters. All interpreters are intended to run in the user's jail. The
 
42
    jail location is provided as an argument to the interpreter but it is up
 
43
    to the individual interpreters to create the jail.
 
44
 
 
45
    req: An IVLE request object.
 
46
    owner: Username of the user who owns the file being served.
 
47
    filename: Filename in the local file system.
 
48
    interpreter: A function object to call.
 
49
    """
 
50
    # Make sure the file exists (otherwise some interpreters may not actually
 
51
    # complain).
 
52
    # Don't test for execute permission, that will only be required for
 
53
    # certain interpreters.
 
54
    if not os.access(filename, os.R_OK):
 
55
        req.throw_error(req.HTTP_NOT_FOUND)
 
56
 
 
57
    # Get the UID of the owner of the file
 
58
    # (Note: files are executed by their owners, not the logged in user.
 
59
    # This ensures users are responsible for their own programs and also
 
60
    # allows them to be executed by the public).
 
61
    try:
 
62
        (_,_,uid,_,_,_,_) = pwd.getpwnam(owner)
 
63
    except KeyError:
 
64
        # The user does not exist. This should have already failed the
 
65
        # previous test.
 
66
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR)
 
67
 
 
68
    # Split up req.path again, this time with respect to the jail
 
69
    (_, jail_dir, path) = studpath.url_to_jailpaths(req.path)
 
70
    path = os.path.join('/', path)
 
71
    (working_dir, _) = os.path.split(path)
 
72
    # jail_dir is the absolute jail directory.
 
73
    # path is the filename relative to the user's jail.
 
74
    # working_dir is the directory containing the file relative to the user's
 
75
    # jail.
 
76
    # (Note that paths "relative" to the jail actually begin with a '/' as
 
77
    # they are absolute in the jailspace)
 
78
 
 
79
    return interpreter(uid, jail_dir, working_dir, path, req)
 
80
 
 
81
class CGIFlags:
 
82
    """Stores flags regarding the state of reading CGI output."""
 
83
    def __init__(self):
 
84
        self.started_cgi_body = False
 
85
        self.got_cgi_headers = False
 
86
        self.wrote_html_warning = False
 
87
        self.linebuf = ""
 
88
        self.headers = {}       # Header names : values
 
89
 
 
90
def execute_cgi(interpreter, trampoline, uid, jail_dir, working_dir,
 
91
                script_path, req):
 
92
    """
 
93
    trampoline: Full path on the local system to the CGI wrapper program
 
94
        being executed.
 
95
    uid: User ID of the owner of the file.
 
96
    jail_dir: Absolute path of owner's jail directory.
 
97
    working_dir: Directory containing the script file relative to owner's
 
98
        jail.
 
99
    script_path: CGI script relative to the owner's jail.
 
100
    req: IVLE request object.
 
101
 
 
102
    The called CGI wrapper application shall be called using popen and receive
 
103
    the HTTP body on stdin. It shall receive the CGI environment variables to
 
104
    its environment.
 
105
    """
 
106
 
 
107
    # Get the student program's directory and execute it from that context.
 
108
    (tramp_dir, _) = os.path.split(trampoline)
 
109
 
 
110
    # TODO: Don't create a file if the body length is known to be 0
 
111
    # Write the HTTP body to a temporary file so it can be passed as a *real*
 
112
    # file to popen.
 
113
    f = os.tmpfile()
 
114
    body = req.read()
 
115
    if body is not None:
 
116
        f.write(body)
 
117
        f.flush()
 
118
        f.seek(0)       # Rewind, for reading
 
119
 
 
120
    # usage: tramp uid jail_dir working_dir script_path
 
121
    pid = subprocess.Popen(
 
122
        [trampoline, str(uid), jail_dir, working_dir, interpreter,
 
123
        script_path],
 
124
        stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
 
125
        cwd=tramp_dir)
 
126
 
 
127
    # process_cgi_line: Reads a single line of CGI output and processes it.
 
128
    # Prints to req, and also does fancy HTML warnings if Content-Type
 
129
    # omitted.
 
130
    cgiflags = CGIFlags()
 
131
 
 
132
    # Read from the process's stdout into req
 
133
    data = pid.stdout.read(CGI_BLOCK_SIZE)
 
134
    while len(data) > 0:
 
135
        process_cgi_output(req, data, cgiflags)
 
136
        data = pid.stdout.read(CGI_BLOCK_SIZE)
 
137
 
 
138
    # If we haven't processed headers yet, now is a good time
 
139
    if not cgiflags.started_cgi_body:
 
140
        process_cgi_output(req, '\n', cgiflags)
 
141
 
 
142
    # If we wrote an HTML warning header, write the footer
 
143
    if cgiflags.wrote_html_warning:
 
144
        req.write("""</pre>
 
145
  </div>
 
146
</body>
 
147
</html>""")
 
148
 
 
149
def process_cgi_output(req, data, cgiflags):
 
150
    """Processes a chunk of CGI output. data is a string of arbitrary length;
 
151
    some arbitrary chunk of output written by the CGI script."""
 
152
    if cgiflags.started_cgi_body:
 
153
        if cgiflags.wrote_html_warning:
 
154
            # HTML escape text if wrote_html_warning
 
155
            req.write(cgi.escape(data))
 
156
        else:
 
157
            req.write(data)
 
158
    else:
 
159
        # Break data into lines of CGI header data. 
 
160
        linebuf = cgiflags.linebuf + data
 
161
        # First see if we can split all header data
 
162
        split = linebuf.split('\r\n\r\n', 1)
 
163
        if len(split) == 1:
 
164
            # Allow UNIX newlines instead
 
165
            split = linebuf.split('\n\n', 1)
 
166
        if len(split) == 1:
 
167
            # Haven't seen all headers yet. Buffer and come back later.
 
168
            cgiflags.linebuf = linebuf
 
169
            return
 
170
 
 
171
        headers = split[0]
 
172
        data = split[1]
 
173
        cgiflags.linebuf = ""
 
174
        cgiflags.started_cgi_body = True
 
175
        # Process all the header lines
 
176
        split = headers.split('\r\n', 1)
 
177
        if len(split) == 1:
 
178
            split = headers.split('\n', 1)
 
179
        while True:
 
180
            process_cgi_header_line(req, split[0], cgiflags)
 
181
            if len(split) == 1: break
 
182
            headers = split[1]
 
183
            if cgiflags.wrote_html_warning:
 
184
                # We're done with headers. Treat the rest as data.
 
185
                data = headers + '\n' + data
 
186
                break
 
187
            split = headers.split('\r\n', 1)
 
188
            if len(split) == 1:
 
189
                split = headers.split('\n', 1)
 
190
 
 
191
        # Check to make sure the required headers were written
 
192
        if cgiflags.wrote_html_warning:
 
193
            # We already reported an error, that's enough
 
194
            pass
 
195
        elif "Content-Type" in cgiflags.headers:
 
196
            pass
 
197
        elif "Location" in cgiflags.headers:
 
198
            if ("Status" in cgiflags.headers and req.status >= 300
 
199
                and req.status < 400):
 
200
                pass
 
201
            else:
 
202
                message = """You did not write a valid status code for
 
203
the given location. To make a redirect, you may wish to try:</p>
 
204
<pre style="margin-left: 1em">Status: 302 Found
 
205
Location: &lt;redirect address&gt;</pre>"""
 
206
                write_html_warning(req, message)
 
207
                cgiflags.wrote_html_warning = True
 
208
        else:
 
209
            message = """You did not print a Content-Type header.
 
210
CGI requires that you print a "Content-Type". You may wish to try:</p>
 
211
<pre style="margin-left: 1em">Content-Type: text/html</pre>"""
 
212
            write_html_warning(req, message)
 
213
            cgiflags.wrote_html_warning = True
 
214
 
 
215
        # Call myself to flush out the extra bit of data we read
 
216
        process_cgi_output(req, data, cgiflags)
 
217
 
 
218
def process_cgi_header_line(req, line, cgiflags):
 
219
    """Process a line of CGI header data. line is a string representing a
 
220
    complete line of text, stripped and without the newline.
 
221
    """
 
222
    try:
 
223
        name, value = line.split(':', 1)
 
224
    except ValueError:
 
225
        # No colon. The user did not write valid headers.
 
226
        if len(cgiflags.headers) == 0:
 
227
            # First line was not a header line. We can assume this is not
 
228
            # a CGI app.
 
229
            message = """You did not print a CGI header.
 
230
CGI requires that you print a "Content-Type". You may wish to try:</p>
 
231
<pre style="margin-left: 1em">Content-Type: text/html</pre>"""
 
232
        else:
 
233
            # They printed some header at least, but there was an invalid
 
234
            # header.
 
235
            message = """You printed an invalid CGI header. You need to leave
 
236
a blank line after the headers, before writing the page contents."""
 
237
        write_html_warning(req, message)
 
238
        cgiflags.wrote_html_warning = True
 
239
        # Handle the rest of this line as normal data
 
240
        process_cgi_output(req, line + '\n', cgiflags)
 
241
        return
 
242
 
 
243
    # Read CGI headers
 
244
    value = value.strip()
 
245
    if name == "Content-Type":
 
246
        req.content_type = value
 
247
    elif name == "Location":
 
248
        req.location = value
 
249
    elif name == "Status":
 
250
        # Must be an integer, followed by a space, and then the status line
 
251
        # which we ignore (seems like Apache has no way to send a custom
 
252
        # status line).
 
253
        try:
 
254
            req.status = int(value.split(' ', 1)[0])
 
255
        except ValueError:
 
256
            message = """The "Status" CGI header was invalid. You need to
 
257
print a number followed by a message, such as "302 Found"."""
 
258
            write_html_warning(req, message)
 
259
            cgiflags.wrote_html_warning = True
 
260
            # Handle the rest of this line as normal data
 
261
            process_cgi_output(req, line + '\n', cgiflags)
 
262
    else:
 
263
        # Generic HTTP header
 
264
        # FIXME: Security risk letting users write arbitrary headers?
 
265
        req.headers_out[name] = value
 
266
    cgiflags.headers[name] = value
 
267
 
 
268
def write_html_warning(req, text):
 
269
    """Prints an HTML warning about invalid CGI interaction on the part of the
 
270
    user. text may contain HTML markup."""
 
271
    req.content_type = "text/html"
 
272
    req.write("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 
273
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
274
<html xmlns="http://www.w3.org/1999/xhtml">
 
275
<head>
 
276
  <meta http-equiv="Content-Type"
 
277
    content="text/html; charset=utf-8" />
 
278
</head>
 
279
<body style="margin: 0; padding: 0; font-family: sans-serif;">
 
280
  <div style="background-color: #faa; border-bottom: 1px solid black;
 
281
    padding: 8px;">
 
282
    <p><strong>Warning</strong>: %s
 
283
  </div>
 
284
  <div style="margin: 8px;">
 
285
    <pre>
 
286
""" % text)
 
287
 
 
288
location_cgi_python = os.path.join(conf.ivle_install_dir,
 
289
    "bin/trampoline")
 
290
 
 
291
# Mapping of interpreter names (as given in conf/app/server.py) to
 
292
# interpreter functions.
 
293
 
 
294
interpreter_objects = {
 
295
    'cgi-python'
 
296
        : functools.partial(execute_cgi, "/usr/bin/python",
 
297
            location_cgi_python),
 
298
    # Should also have:
 
299
    # cgi-generic
 
300
    # python-server-page
 
301
}
 
302