How to marshal a function?
François Pinard
pinard at iro.umontreal.ca
Tue Nov 20 18:12:46 EST 2001
[Cliff Wells]
> As far as whether this will provide sufficient security, that's obviously
> more difficult to say. It was merely my original intention to point out
> the possibility of a security hole (there was no information regarding how
> he was using this application in the first few posts), it seeming likely
> to me that more information regarding his particular application would be
> forthcoming, but the discussion didn't continue in this vein, so this was
> never addressed.
OK. I just translated the doc-strings and comments to English, so I can
share the little tool with you. That should allow you people to evaluate
this module's security, or lack thereof, and advise me!
Besides, I think the tool may be useful in itself, to some of you. For one,
I use this module within a bigger setup meant for administrating systems
and user accounts for many machines at once, and in parallel.
-------------- next part --------------
#!/usr/bin/env python
# Copyright ? 2001 Progiciels Bourbeau-Pinard inc.
# Fran?ois Pinard <pinard at iro.umontreal.ca>, 2001.
"""\
Python services on a remote machine.
To each Server instance is associated an `ssh' link towards a remote server
program. That remote server, which gets automatically installed, is able to
evaluate Python expressions, apply functions or execute Python statements,
on demand, within in a special evaluation context held within that server.
The `pickle' module is used for all transit to or from the server, so the
programmer should restrain him/herself to Python values that can be pickled.
Here is a simplistic example. Suppose `cliff' is an Internet host for
which we already have immediate SSH access through the proper key setup.
To get `cliff' to compute `2 + 3', a Python expression, one uses this:
import remote
server = remote.Server('cliff')
print server.eval('2 + 3')
server.complete()
If the host name is missing or None, the current host is directly used,
without installing nor using a remote server.
The server is installed as `~/.python-remote-VERSION' on the remote host
(VERSION identifies the protocol) and left there afterwards. If the server
already exists, it is merely reused if it identifies itself correctly.
Typically, the link is kept opened to service many requests which depend
on the remote machine, either for its computing power, its file system,
or other idiosyncrasies, and closed once the overall task is completed.
"""
import string, sys
error = 'Remote error'
APPLY_CODE, EVAL_CODE, EXECUTE_CODE = range(3)
NORMAL_RETURN, ERROR_RETURN = range(2)
class run:
version = 1
header = "Python `remote' server, protocol version %d" % version
script = '.python-remote-%d' % version
def main(*arguments):
import getopt
options, arguments = getopt.getopt(arguments, '')
for option, value in options:
pass
execute_server()
class Server:
def __init__(self, host=None):
if host is None:
self.child = None
self.context = {}
return
import os, popen2
self.host = host
name = __file__
if name[-4:] == '.pyc':
name = name[:-4] + '.py'
command = 'ssh -x %s python %s' % (host, run.script)
for counter in range(2):
self.child = popen2.Popen3(command)
text = self.receive_text()
if text == '%s\n' % run.header:
return
if text is None:
sys.stderr.write("Oops! Installing Python server on `%s'\n"
% host)
os.system('scp -pq %s %s:%s' % (name, host, run.script))
assert 0, "Unable to install `%s' on `%s'." % (name, host)
def complete(self):
if self.child is not None:
self.send_text('')
text = self.receive_text()
assert text == '', text
self.child = None
def apply(self, text, arguments):
"""\
Evaluate TEXT, which should yield a function on the remote server.
Then apply this function over ARGUMENTS, and return the function value.
"""
if self.child is None:
return apply(eval(text, globals(), self.context), arguments)
return self.round_trip((APPLY_CODE, (text, arguments)))
def eval(self, text):
"""\
Get the remote server to evaluate TEXT as an expression, and return its value.
"""
if self.child is None:
return eval(text, globals(), self.context)
return self.round_trip((EVAL_CODE, text))
def execute(self, text):
"""\
Execute TEXT as Python statements on the remote server. Return None.
"""
if self.child is None:
exec text in globals(), self.context
return
return self.round_trip((EXECUTE_CODE, text))
def round_trip(self, request):
import base64, pickle, zlib
text = base64.encodestring(zlib.compress(pickle.dumps(request)))
self.send_text(text)
text = self.receive_text()
if text is None:
return None
code, value = pickle.loads(zlib.decompress(base64.decodestring(text)))
if code == ERROR_RETURN:
raise error, value
return value
def send_text(self, text):
assert self.child.poll() == -1, \
"%s: Python server has been interrupted." % self.host
self.child.tochild.write(text + '\n')
self.child.tochild.flush()
def receive_text(self):
assert self.child.poll() == -1, \
"%s: Python server has been interrupted." % self.host
lines = []
while 1:
line = self.child.fromchild.readline()
if not line:
break
if line == '\n':
return string.join(lines, '')
lines.append(line)
def execute_server():
"""\
Python remote server proper.
Here is a description of the communication protocol. The server identifies
itself on a single stdout line, followed by an empty line. It then enters a
loop reading one request on stdin terminated by an empty line, and writing
the reply on stdout, followed by an empty line. Requests and replies are
compressed pickles which are Base64-coded over possibly multiple lines.
All requests are processed within a same single context for local variables.
An empty request produces an empty reply and the termination of this server.
"""
import StringIO, base64, pickle, traceback, zlib
context = {}
readline = sys.stdin.readline
write = sys.stdout.write
flush = sys.stdout.flush
write('%s\n\n' % run.header)
flush()
lines = []
while 1:
line = readline()
if line != '\n':
lines.append(line)
continue
text = string.join(lines, '')
if text == '':
write('\n')
break
lines = []
request = pickle.loads(zlib.decompress(base64.decodestring(text)))
code, text = request
try:
if code == APPLY_CODE:
text, arguments = text
code = NORMAL_RETURN
value = apply(eval(text, globals(), context), arguments)
elif code == EVAL_CODE:
code = NORMAL_RETURN
value = eval(text, globals(), context)
else:
exec text in globals(), context
code = NORMAL_RETURN
value = None
except:
message = StringIO.StringIO()
traceback.print_exc(file=message)
code = ERROR_RETURN
value = message.getvalue()
write(base64.encodestring(zlib.compress(pickle.dumps((code, value)))))
write('\n')
flush()
if __name__ == '__main__':
apply(main, sys.argv[1:])
-------------- next part --------------
--
Fran?ois Pinard http://www.iro.umontreal.ca/~pinard
More information about the Python-list
mailing list