Part 1: Introduction to Classic RPyC
We’ll kick-start the tutorial with what is known as classic-style RPyC, i.e., the methodology of RPyC 2.60. Since RPyC 3 is a complete redesign of the library, there are some minor changes, but if you were familiar with RPyC 2.60, you’ll feel right at home. And even if you were not – we’ll make sure you feel at home in a moment ;)
Running a Server
Let’s start with the basics: running a server. In this tutorial we’ll run both the server and
the client on the same machine (the localhost
). The classic server can be
started using:
$ python bin/rpyc_classic.py
INFO:SLAVE/18812:server started on [127.0.0.1]:18812
This shows the parameters this server is running with:
SLAVE
indicates theSlaveService
(you’ll learn more about services later on), and[127.0.0.1]:18812
is the address on which the server binds, in this case the server will only accept connections from localhost. If you run a server with--host 0.0.0.0
, you are free for arbitrary code execution from anywhere.
Running a Client
The next step is running a client which connects to the server. The code needed to create a connection to the server is quite simple, you’d agree
import rpyc
conn = rpyc.classic.connect("localhost")
If your server is not running on the default port (TCP 18812
), you’ll have
to pass the port=
parameter to classic.connect()
.
The modules
Namespace
The modules
property of connection objects exposes the server’s
module-space, i.e., it lets you access remote modules. Here’s how:
rsys = conn.modules.sys # remote module on the server!
This dot notation only works for top level modules. Whenever you would require a nested import for modules contained within a package, you have to use the bracket notation to import the remote module, e.g.:
minidom = conn.modules["xml.dom.minidom"]
With this alone you are already set to do almost anything. For example, here is how you see the server’s command line:
>>> rsys.argv
['bin/rpyc_classic.py']
…add module search paths for the server’s import mechanism:
>>> rsys.path.append('/tmp/totally-secure-package-location)
…change the current working directory of the server process:
>>> conn.modules.os.chdir('..')
…or even print something on the server’s stdout:
>>> print("Hello World!", file=conn.modules.sys.stdout)
The builtins
Namespace
The builtins
property of classic connection exposes all builtin functions
available in the server’s python environment. You could use it for example to
access a file on the server:
>>> f = conn.builtins.open('/home/oblivious/.ssh/id_rsa')
>>> f.read()
'-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA0...XuVmz/ywq+5m\n-----END RSA PRIVATE KEY-----\n'
Ooopsies, I just leaked my private key…;)
The eval
and execute
Methods
If you are not satisfied already, here is more: Classic connections also have
properties eval
and execute
that allow you to directly evaluate
arbitrary expressions or even execute arbitrary statements on the server.
For example:
>>> conn.execute('import math')
>>> conn.eval('2*math.pi')
6.283185307179586
But wait, this requires that rpyc classic connections have some notion of
global variables, how can you see them? They are accessible via the
namespace
property that will be initialized as empty dictionary for every
new connection. So, after our import, we now have:
>>> conn.namespace
{'__builtins__': <...>, 'math': <...>}
The aware reader will have noticed that neither of these shenanigans are
strictly needed, as the same functionality could be achieved by using the
conn.builtins.compile()
function, which is also accessible via
conn.modules.builtins.compile()
, and manually feeding it with a remotely
created dict.
That’s true, but we sometimes like a bit of sugar;)
The teleport
method
There is another interesting method that allows you to transmit functions to the other sides and execute them over there:
>>> def square(x):
... return x**2
>>> fn = conn.teleport(square)
>>> fn(2)
This calculates the square of two as expected, but the computation takes place on the remote!
Furthermore, teleported functions are automatically defined in the remote namespace:
>>> conn.eval('square(3)')
9
>>> conn.namespace['square'] is fn
True
And the teleported code can also access the namespace:
>>> conn.execute('import sys')
>>> version = conn.teleport(lambda: print(sys.version_info))
>>> version()
prints the version on the remote terminal.
Note that currently it is not possible to teleport arbitrary functions, in particular there can be issues with closures to non-trivial objects. In case of problems it may be worth taking a look at external libraries such as dill.
Continue to Part 2: Netrefs and Exceptions…