1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
.. _tut1:
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 the ``SlaveService`` (you'll learn more about
:ref:`services <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 :func:`.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_.
.. _dill: https://pypi.org/project/dill/
Continue to :ref:`tut2`...
|