15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
# Author: Matt Giuca, Tom Conway
18
# Author: Matt Giuca, Tom Conway, Will Grant
20
'''Python console RPC service.
22
Provides an HTTP RPC interface to a Python console process.
32
from common import (util, studpath, chat)
36
trampoline_path = os.path.join(conf.ivle_install_dir, "bin/trampoline")
37
python_path = "/usr/bin/python" # Within jail
38
console_dir = "/opt/ivle/scripts" # Within jail
39
console_path = "/opt/ivle/scripts/python-console" # Within jail
42
"""Handler for the Console Service AJAX backend application."""
43
if len(req.path) > 0 and req.path[-1] == os.sep:
47
# The path determines which "command" we are receiving
48
if req.path == "start":
50
elif req.path == "interrupt":
51
handle_chat(req, kind='interrupt')
52
elif req.path == "restart":
53
handle_chat(req, kind='restart')
54
elif req.path == "chat":
56
elif req.path == "block":
57
handle_chat(req, kind="block")
59
req.throw_error(req.HTTP_BAD_REQUEST)
61
def handle_start(req):
62
# Changes the state on the server - must be POST
63
if req.method != "POST":
64
req.throw_error(req.HTTP_BAD_REQUEST)
66
# See if we have been given extra params
67
fields = req.get_fieldstorage()
69
startdir = fields.getfirst("startdir").value
70
working_dir = os.path.join("/home", req.user.login, startdir)
71
except AttributeError:
72
working_dir = os.path.join("/home", req.user.login)
74
# Get the UID of the logged-in user
77
# Set request attributes
78
req.content_type = "text/plain"
79
req.write_html_head_foot = False
82
jail_path = os.path.join(conf.jail_base, req.user.login)
83
(host, port, magic) = start_console(uid, jail_path, working_dir)
85
# Assemble the key and return it.
86
key = cjson.encode({"host": host, "port": port, "magic": magic})
87
req.write(cjson.encode(key.encode("hex")))
89
def handle_chat(req, kind = "chat"):
90
# The request *should* have the following four fields:
91
# host, port, magic: Host and port where the console server lives,
92
# and the secret to use to digitally sign the communication with the
94
# text: Fields to pass along to the console server
95
# It simply acts as a proxy to the console server
96
if req.method != "POST":
97
req.throw_error(req.HTTP_BAD_REQUEST)
98
jail_path = os.path.join(conf.jail_base, req.user.login)
99
working_dir = os.path.join("/home", req.user.login) # Within jail
100
uid = req.user.unixid
101
fields = req.get_fieldstorage()
103
key = cjson.decode(fields.getfirst("key").value.decode("hex"))
107
except AttributeError:
108
# Any of the getfirsts returned None
109
req.throw_error(req.HTTP_BAD_REQUEST)
110
# If text is None, it was probably just an empty line
112
text = fields.getfirst("text").value.decode('utf-8')
113
except AttributeError:
116
msg = {'cmd':kind, 'text':text}
118
response = chat.chat(host, port, msg, magic, decode = False)
120
# Snoop the response from python-console to check that it's valid
122
decoded_response = cjson.decode(response)
123
except cjson.DecodeError:
124
# Could not decode the reply from the python-console server
125
decoded_response = {"restart":
126
"Communication to console process lost"}
127
if "restart" in decoded_response:
128
response = restart_console(uid, jail_path, working_dir,
129
decoded_response["restart"])
131
except socket.error, (enumber, estring):
132
if enumber == errno.ECONNREFUSED:
133
# Timeout: Restart the session
134
response = restart_console(uid, jail_path, working_dir,
135
"The IVLE console has timed out due to inactivity")
34
from ivle.webapp.base.rest import JSONRESTView, named_operation
35
from ivle.webapp.errors import BadRequest
37
# XXX: Should be RPC view, with actions in URL?
38
class ConsoleServiceRESTView(JSONRESTView):
39
'''An RPC interface to a Python console.'''
40
def get_permissions(self, user):
137
# Some other error - probably serious
138
raise socket.error, (enumber, estring)
140
req.content_type = "text/plain"
143
def start_console(uid, jail_path, working_dir):
144
"""Starts up a console service for user uid, inside chroot jail jail_path
145
with work directory of working_dir
146
Returns a tupple (host, port, magic)
149
# TODO: Figure out the host name the console server is running on.
150
host = socket.gethostname()
154
magic = md5.new(uuid.uuid4().bytes).digest().encode('hex')
156
# Try to find a free port on the server.
157
# Just try some random ports in the range [3000,8000)
158
# until we either succeed, or give up. If you think this
159
# sounds risky, it isn't:
160
# For N ports (e.g. 5000) with k (e.g. 100) in use, the
161
# probability of failing to find a free port in t (e.g. 5) tries
162
# is (k / N) ** t (e.g. 3.2*10e-9).
166
port = int(random.uniform(3000, 8000))
168
# Start the console server (port, magic)
169
# trampoline usage: tramp uid jail_dir working_dir script_path args
170
# console usage: python-console port magic
171
cmd = ' '.join([trampoline_path, str(uid), jail_path,
172
console_dir, python_path, console_path,
173
str(port), str(magic), working_dir])
183
# If we can't start the console after 5 attemps (can't find a free port
184
# during random probing, syntax errors, segfaults) throw an exception.
186
raise Exception, "unable to start console service!"
188
return (host, port, magic)
190
def restart_console(uid, jail_path, working_dir, reason):
46
@named_operation('use')
47
def start(self, req, cwd=''):
48
working_dir = os.path.join("/home", req.user.login, cwd)
53
jail_path = os.path.join(req.config['paths']['jails']['mounts'],
55
cons = ivle.console.Console(req.config, uid, jail_path, working_dir)
57
# Assemble the key and return it. Yes, it is double-encoded.
58
return {'key': cjson.encode({"host": cons.host,
60
"magic": cons.magic}).encode('hex')}
62
@named_operation('use')
63
def chat(self, req, key, text='', kind="chat"):
64
# The request *should* have the following four fields:
65
# key: Hex JSON dict of host and port where the console server lives,
66
# and the secret to use to digitally sign the communication with the
68
# text: Fields to pass along to the console server
69
# It simply acts as a proxy to the console server
72
keydict = cjson.decode(key.decode('hex'))
73
host = keydict['host']
74
port = keydict['port']
75
magic = keydict['magic']
77
raise BadRequest("Invalid console key.")
79
jail_path = os.path.join(req.config['paths']['jails']['mounts'],
81
working_dir = os.path.join("/home", req.user.login) # Within jail
84
# XXX: JSONRESTView should do this for us.
85
text = text.decode('utf-8')
87
msg = {'cmd':kind, 'text':text}
89
json_response = ivle.chat.chat(host, port, msg, magic,decode=False)
91
# Snoop the response from python-console to check that it's valid
93
response = cjson.decode(json_response)
94
except cjson.DecodeError:
95
# Could not decode the reply from the python-console server
96
response = {"terminate":
97
"Communication to console process lost"}
98
if "terminate" in response:
99
response = restart_console(req.config, uid, jail_path,
100
working_dir, response["terminate"])
101
except socket.error, (enumber, estring):
102
if enumber == errno.ECONNREFUSED:
103
# Timeout: Restart the session
104
response = restart_console(req.config, uid, jail_path,
106
"The IVLE console has timed out due to inactivity")
107
elif enumber == errno.ECONNRESET:
108
# Communication issue: Restart the session
109
response = restart_console(req.config, uid, jail_path,
111
"Connection with the console has been reset")
113
# Some other error - probably serious
114
raise socket.error, (enumber, estring)
118
def restart_console(config, uid, jail_path, working_dir, reason):
191
119
"""Tells the client that it must be issued a new console since the old
192
120
console is no longer availible. The client must accept the new key.
193
121
Returns the JSON response to be given to the client.
195
123
# Start a new console server console
196
(host, port, magic) = start_console(uid, jail_path, working_dir)
124
cons = ivle.console.Console(config, uid, jail_path, working_dir)
198
126
# Make a JSON object to tell the browser to restart its console client
199
new_key = cjson.encode({"host": host, "port": port, "magic": magic})
202
"key": new_key.encode("hex"),
205
return cjson.encode(json_restart)
127
new_key = cjson.encode(
128
{"host": cons.host, "port": cons.port, "magic": cons.magic})
130
return {"restart": reason, "key": new_key.encode("hex")}