35
36
# TODO: Make progressive output work
36
37
# Question: Will having a large buffer size stop progressive output from
37
38
# working on smaller output
39
40
CGI_BLOCK_SIZE = 65535
41
PATH = "/usr/local/bin:/usr/bin:/bin"
41
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True):
43
def interpret_file(req, owner, jail_dir, filename, interpreter, gentle=True,
42
45
"""Serves a file by interpreting it using one of IVLE's builtin
43
46
interpreters. All interpreters are intended to run in the user's jail. The
44
47
jail location is provided as an argument to the interpreter but it is up
73
79
# (Note that paths "relative" to the jail actually begin with a '/' as
74
80
# they are absolute in the jailspace)
76
return interpreter(owner.unixid, jail_dir, working_dir, filename_abs, req,
82
return interpreter(owner, jail_dir, working_dir, filename_abs, req,
83
gentle, overrides=overrides)
80
86
"""Stores flags regarding the state of reading CGI output.
89
95
self.headers = {} # Header names : values
91
def execute_cgi(interpreter, uid, jail_dir, working_dir, script_path,
97
def execute_cgi(interpreter, owner, jail_dir, working_dir, script_path,
98
req, gentle, overrides=None):
94
100
trampoline: Full path on the local system to the CGI wrapper program
96
uid: User ID of the owner of the file.
102
owner: User object of the owner of the file.
97
103
jail_dir: Absolute path of owner's jail directory.
98
104
working_dir: Directory containing the script file relative to owner's
100
106
script_path: CGI script relative to the owner's jail.
101
107
req: IVLE request object.
109
overrides: A dict mapping env var names to strings, to override arbitrary
110
environment variables in the resulting CGI environent.
103
112
The called CGI wrapper application shall be called using popen and receive
104
113
the HTTP body on stdin. It shall receive the CGI environment variables to
129
138
f.seek(0) # Rewind, for reading
131
140
# Set up the environment
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():
137
for (k,v) in req.get_cgi_environ().items():
139
fixup_environ(req, script_path)
141
environ = cgi_environ(req, script_path, owner, overrides=overrides)
141
143
# usage: tramp uid jail_dir working_dir script_path
142
cmd_line = [trampoline, str(uid), req.config['paths']['jails']['mounts'],
143
req.config['paths']['jails']['src'],
144
req.config['paths']['jails']['template'],
145
jail_dir, working_dir, interpreter, 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]
146
149
# Popen doesn't like unicode strings. It hateses them.
147
150
cmd_line = [(s.encode('utf-8') if isinstance(s, unicode) else s)
148
151
for s in cmd_line]
149
152
pid = subprocess.Popen(cmd_line,
150
153
stdin=f, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
153
# Restore the environment
154
for k in os.environ.keys():
156
for (k,v) in old_env.items():
154
cwd=tramp_dir, env=environ)
159
156
# We don't want any output! Bail out after the process terminates.
226
223
if len(split) == 1:
227
224
split = headers.split('\n', 1)
229
# Is this an internal IVLE error condition?
230
hs = cgiflags.headers
231
if 'X-IVLE-Error-Type' in hs:
232
t = hs['X-IVLE-Error-Type']
233
if t == IVLEError.__name__:
234
raise IVLEError(int(hs['X-IVLE-Error-Code']),
235
hs['X-IVLE-Error-Message'])
226
# If not executing in gentle mode (which presents CGI violations
227
# to users nicely), check if this an internal IVLE error
229
if not cgiflags.gentle:
230
hs = cgiflags.headers
231
if 'X-IVLE-Error-Type' in hs:
238
233
raise IVLEJailError(hs['X-IVLE-Error-Type'],
239
234
hs['X-IVLE-Error-Message'],
240
235
hs['X-IVLE-Error-Info'])
242
raise IVLEError(500, 'bad error headers written by CGI')
237
raise AssertionError("Bad error headers written by CGI.")
244
239
# Check to make sure the required headers were written
245
240
if cgiflags.wrote_html_warning or not cgiflags.gentle:
299
294
process_cgi_output(req, line + '\n', cgiflags)
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)):
302
if not cgiflags.gentle:
303
message = """An unexpected server error has occured."""
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>( ) < > @ , ; : \\ " / [ ] ? = { } <em>SPACE
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)
302
317
# Read CGI headers
303
318
value = value.strip()
304
319
if name == "Content-Type":
361
376
# python-server-page
364
def fixup_environ(req, script_path):
365
"""Assuming os.environ has been written with the CGI variables from
366
apache, make a few changes for security and correctness.
379
def cgi_environ(req, script_path, user, overrides=None):
380
"""Gets CGI variables from apache and makes a few changes for security and
368
383
Does not modify req, only reads it.
385
overrides: A dict mapping env var names to strings, to override arbitrary
386
environment variables in the resulting CGI environent.
371
389
# Comments here are on the heavy side, explained carefully for security
372
390
# reasons. Please read carefully before making changes.
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():
374
397
# Remove DOCUMENT_ROOT and SCRIPT_FILENAME. Not part of CGI spec and
375
398
# exposes unnecessary details about server.
444
471
for s in cmd_line]
445
472
proc = subprocess.Popen(cmd_line,
446
473
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
447
stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True)
474
stderr=subprocess.PIPE, cwd=tramp_dir, close_fds=True,
475
env={'HOME': os.path.join('/home', user.login),
478
'LOGNAME': user.login})
449
480
(stdout, stderr) = proc.communicate()
450
481
exitcode = proc.returncode
452
483
if exitcode != 0:
453
raise ExecutionError('subprocess ended with code %d, stderr %s' %
454
(exitcode, proc.stderr.read()))
484
raise ExecutionError('subprocess ended with code %d, stderr: "%s"' %
455
486
return (stdout, stderr)
488
def jail_call(req, cgi_script, script_name, query_string=None,
489
request_method="GET", extra_overrides=None):
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.
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.
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.
508
Returns a triple (status_code, content_type, contents).
510
interp_object = interpreter_objects["cgi-python"]
511
user_jail_dir = os.path.join(req.config['paths']['jails']['mounts'],
514
"SCRIPT_NAME": script_name,
515
"QUERY_STRING": query_string,
516
"REQUEST_URI": "%s%s%s" % (script_name, "?" if query_string else "",
518
"REQUEST_METHOD": request_method,
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()
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.)
533
def __init__(self, req):
534
StringIO.StringIO.__init__(self)
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)