Using Perspective Broker: the walking tour
- Foreword
- Introduction: why use PB?
- Class roadmap
- Why are there so many classes?
- Which ones am I supposed to subclass?
- Basic remote method invocation
- Passing more references
- References can come back to you
- Beyond a single "root" object
- Flavors of remotely-accessible objects
- Wrapup before twisted.cred
- Authentication, Identities, and Perspectives: twisted.cred
Foreword
This document is intended to be a tutorial for getting familiar with Twisted's "Perspective Broker" service. The framework is powerful and flexible, and I could tell it was something I wanted to be able to use, but I found it difficult to learn how to use it, either by reading the HOWTO docs, the code's docstrings, or the few examples I could find. All of them had an implicit assumption that the reader already understood some basic concepts, an understanding I did not have. This is a recurring difficulty with documentation: the only people who know enough to write good docs already know too much to be able to guess what a beginner won't get. From the documentation-writing point of view, this means that "ignorance" is a valuable commodity: it lets you know what parts need to be documented better, and which questions to ask.
I have found that trying to learn Twisted is a bit like looking at that silhouette drawing that can either be seen as a vase or as two faces: there's a cognitive leap you need to make that is difficult for some people (certainly for me), but after you manage it, everything makes perfect sense and you can easily see how all the parts fit together. I've found that when I do finally manage to see the two faces (or the vase), it falls so strongly into place that I find it hard to remember what I had been confused about before. At that point I've used up my ignorance, and then find it hard to figure out what hints or docs would have been useful to me when I got stuck.
So I am writing this just after I hit this point of enlightenment with Twisted, in the hope that I can document enough of the process I went through to be useful to someone else following the same path.
What I have discovered is that, now (after I think I know what's going on), the HOWTO docs that come with Twisted make much more sense to me. I know that they didn't before. So perhaps I've already lost that precious ignorance that makes it possible to write useful docs. But it seems that this text overlaps the HOWTO docs in significant places. So perhaps this document is best used in conjunction with the howtos.
Introduction: why use Perspective Broker?
So, you've read through the "Writing Servers" page, and now you're a skilled pro at implementing protocols by writing subclasses of twisted.internet.Protocol. You discovered that most of /etc/services has already been implemented somewhere in twisted/protocols/*.py, so you went out to find new weird ones to write: an NTP server, a DHCP implementation, TFTP, and then Gopher because you couldn't find anything else that needed doing.
But suppose you find yourself in control of both ends of the wire: you have two programs that need to talk to each other, and you get to use any protocol you want. If you can think of your problem in terms of objects that need to make method calls on each other, then chances are good that you can use twisted's Perspective Broker protocol rather than trying to shoehorn your needs into something like HTTP, or implementing yet another RPC mechanism.
(most of Twisted is like this. hell, most of unix is like this: if *you* think it would be useful, someone else has probably thought that way in the past, and acted on it, and you can take advantage of the tool they created to solve the same problem you're facing now).
The Perspective Broker system (abbreviated "PB", spawning numerous sandwich-related puns) is based upon a few central concepts:
- serialization: taking fairly arbitrary objects and types, turning them into a chunk of bytes, sending them over a wire, then reconstituting them on the other end. By keeping careful track of object ids, the serialized objects can contain references to other objects and the remote copy will still be useful.
- remote method calls: doing something to a local object and causing a method to get run on a distant one. The local object is called a RemoteReference, and you "do something" by running its .callRemote method.
This document will contain several examples that will (hopefully) appear redundant and verbose once you've figured out what's going on. To begin with, much of the code will just be labeled "magic": don't worry about how these parts work yet. It will be explained more fully later.
At several points there are footnotes that explain implementation details or exceptions to the rules just presented. On your first pass through the text, you should probably ignore these. Think of them as goodies to explore later, after you've gotten the basic concepts down.
Class Roadmap
To start with, here are the major classes involved in PB, with links to the file where they are defined (all of which are under twisted/, of course). Don't worry about understanding what they all do yet: it's easier to figure them out through their interaction than explaining them one at a time.
- Application: internet/app.py
- Authorizer: cred/authorizer.py
- Identity: cred/identity.py
- Service: cred/service.py
- MultiService: internet/app.py
- Factory: internet/protocol.py
- BrokerFactory: spread/pb.py
- Broker: spread/pb.py
- AuthRoot: spread/pb.py
- Perspective: spread/pb.py, subclassed from Perspective in cred/perspective.py
Other classes that are involved at some point:
- RemoteReference: spread/pb.py
- pb.Root: spread/pb.py, actually defined in spread/flavors.py
- pb.Referenceable: spread/pb.py, actually defined in spread/flavors.py
Why are there so many classes?
It's easy to get confused by the large variety of classes, especially when you see that certain functionality has been distributed up among several small classes instead of being lumped into one. What you're seeing is the result of several code refactorings, where highly skilled engineers kept undistracted in their locked cells (in the Engineering Sub-Basement deep below Twisted Matrix Laboratories), using precision machinery and the very latest technology, have painstakingly arranged and rearranged the class structure to the point that that common cases are handled by the standard code, but yet the classes are arranged just right, so that you and other developers can make useful changes by only modifying one or two methods in your subclass.
Basically: the Twisted developers looked at the set of problems they were trying to solve, and put together a scheme that took care of it. Then someone else came along and said "great, but how do I use your framework to solve this other problem too?". Code gets added. Then someone looks at the code and realizes that the needs imposed by the new problems can be generalized, that a clean solution is to break up the functionality into two parts: one that remains the same for both problems, and another that can be changed (probably subclassed) to handle the unique needs of the second. The imaginary dotted lines that break up the problem into smaller subproblems have been rearranged to minimize the number of components that need to be changed: when it is done without changing the overall behavior, this is called Refactoring. (When it does change the behavior, it is called Rewriting, and isn't nearly as cool). Repeat this cycle many times and you get a collection of classes that can solve a wide variety of distributed computing problems with a minimum of new code.
So which ones am I supposed to subclass?
That's a tricky one. Technically you can subclass anything you want, but techically you could also write a whole new framework, which would just waste a lot of time.. Knowing which classes are useful to change (by making subclasses) is one of the bits of knowledge you pick up after using Twisted for a few weeks. Here are some hints to get started:
- Protocol: subclass this if you need to implement a new protocol on the wire, like HTTP or SMTP (except that almost all of the standard ones are already implemented). You might also subclass one of the standard implementations if you want to change its back-end behavior: make an SMTP server which actually stores the messages in files instead of mailing them, or a Finger server that returns random messages instead of current login status.
- pb.Root, pb.Referenceable: you'll subclass these to make remotely-referenceable objects using PB. You don't need to change any of the existing behavior, just inherit all of it and add the remotely-accessible methods that you want to export.
- pb.Perspective, pb.Service: you'll probably end up subclassing these when you get into PB programming (with authorization). There are a few methods you'll change, especially with regards to creating new Perspectives.
- Authorizer: subclass this if you want to get users from /etc/passwd, or a database, or LDAP, or other list of usernames and passwords.
XXX: add lists of useful-to-override methods here
Basic remote method invocation
The first example to look at is a complete (although somewhat trivial) application. It uses BrokerFactory() on the server side, and pb.getObjectAt() on the client side.
# Twisted, the Framework of Your Internet # Copyright (C) 2001 Matthew W. Lefkowitz # # This library is free software; you can redistribute it and/or # modify it under the terms of version 2.1 of the GNU Lesser General Public # License as published by the Free Software Foundation. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from twisted.spread import pb from twisted.internet import app class Echoer(pb.Root): def remote_echo(self, st): print 'echoing:', st return st if __name__ == '__main__': appl = app.Application("pbsimple") appl.listenTCP(8789, pb.BrokerFactory(Echoer())) appl.run()
# Twisted, the Framework of Your Internet # Copyright (C) 2001 Matthew W. Lefkowitz # # This library is free software; you can redistribute it and/or # modify it under the terms of version 2.1 of the GNU Lesser General Public # License as published by the Free Software Foundation. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from twisted.spread import pb from twisted.internet import reactor def gotObject(object): print "got object:",object object.callRemote("echo", "hello network").addCallback(gotEcho) def gotEcho(echo): print 'server echoed:',echo reactor.stop() def gotNoObject(reason): print "no object:",reason reactor.stop() pb.getObjectAt("localhost", 8789, 30).addCallbacks(gotObject, gotNoObject) reactor.run()
First we look at the server. This defines an Echoer class (derived from pb.Root), with a method called remote_echo(). pb.Root objects (because of their inheritance of pb.Referenceable, described later) can define methods with names of the form 'remote_*'; a client which obtains a remote reference to that pb.Root object will be able to invoke those methods.
The pb.Root-ish object is given to a pb.BrokerFactory(). This is a Factory object like any other: the Protocol objects it creates for new connections know how to speak the PB protocol. The object you give to pb.BrokerFactory() becomes the "root object", which simply makes it available for the client to retrieve. The client may only request references to the objects you want to provide it: this helps you implement your security model. Because it is so common to export just a single object (and because a remote_* method on that one can return a reference to any other object you might want to give out), the simplest example is one where the BrokerFactory is given the root object, and the client retrieves it.
The client side uses pb.getObjectAt() to make a connection to a given port. This is a convenience function (not a method) which runs through the PB protocol steps necessary to retrieve the root object from a BrokerFactory sitting at the given port.
Because .getObjectAt() has to make a network connection and exchange some data, it may take a while, so it returns a Deferred, to which the gotObject() callback is attached. (See the documentation on "Deferring Execution" for a complete explanation of Deferreds). If and when the connection succeeds and a reference to the remote root object is obtained, this callback is run. The first argument passed to the callback is a remote reference to the distant root object. (you can give other arguments to the callback too, see the other parameters for .addCallback() and .addCallbacks()).
The callback does:
object.callRemote("echo", "hello network")
which causes the server's .remote_echo() method to be invoked. (running .callRemote("boom") would cause .remote_boom() to be run, etc). Again because of the delay involved, callRemote() returns a Deferred. Assuming the remote method was run without causing an exception (including an attempt to invoke an unknown method), the callback attached to that Deferred will be invoked with any objects that were returned by the remote method call.
In this example, the server's Echoer object has a method invoked, exactly as if some code on the server side had done:
echoer_object.remote_echo("hello network")
and from the definition of remote_echo() we see that this just returns the same string it was given: "hello network".
From the client's point of view, the remote call gets another Deferred object instead of that string. callRemote() always returns a Deferred. This is why PB is described as a system for "translucent" remote method calls instead of "transparent" ones: you cannot pretend that the remote object is really local. Trying to do so (as some other RPC mechanisms do, coughCORBAcough) breaks down when faced with the asynchronous nature of the network. Using Deferreds turns out to be a very clean way to deal with the whole thing.
The remote reference object (the one given to getObjectAt()'s success callback) is an instance the RemoteReference class. This means you can use it to invoke methods on the remote object that it refers to. Only instances of RemoteReference are eligible for .callRemote(). The RemoteReference object is the one that lives on the remote side (the client, in this case), not the local side (where the actual object is defined).
In our example, the local object is that Echoer() instance, which inherits from pb.Root, which inherits from pb.Referenceable. It is that Referenceable class that makes the object eligible to be available for remote method calls [1]. If you have an object that is Referenceable, then any client that manages to get a reference to it can invoke any 'remote_*' methods they please. Note: the only thing they can do is invoke those methods. In particular, they cannot access attributes. From a security point of view, you control what they can do by limiting what the remote_* methods can do. Also note: the other classes like Referenceable allow access to other methods, in particular 'perspective_*' and 'view_*' may be accessed. Don't write local-only methods with these names, because then remote callers will be able to do more than you intended. Also also note: the other classes like pb.Copyable do allow access to attributes, but you control which ones they can see.
You don't have to be a pb.Root to be remotely callable, but you do have to be pb.Referenceable. (Objects that inherit from pb.Referenceable but not from pb.Root can be remotely called, but only pb.Root-ish objects can be given to the BrokerFactory.)
Passing more references
Here is an example of using pb.Referenceable in a second class. The second Referenceable object can have remote methods invoked too, just like the first. In this example, the initial root object has a method that returns a reference to the second object.
#! /usr/bin/python from twisted.spread import pb import twisted.internet.app class Two(pb.Referenceable): def remote_three(self, arg): print "Two.three was given", arg class One(pb.Root): def remote_getTwo(self): two = Two() print "returning a Two called", two return two app = twisted.internet.app.Application("pb2server") app.listenTCP(8800, pb.BrokerFactory(One())) app.run()
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor def main(): def1 = pb.getObjectAt("localhost", 8800, 30) def1.addCallbacks(got_obj1, err_obj1) reactor.run() def err_obj1(reason): print "error getting first object", reason reactor.stop() def got_obj1(obj1): print "got first object:", obj1 print "asking it to getTwo" def2 = obj1.callRemote("getTwo") def2.addCallbacks(got_obj2) def got_obj2(obj2): print "got second object:", obj2 print "telling it to do three(12)" obj2.callRemote("three", 12) main()
The root object has a method called 'remote_getTwo', which returns the Two() instance. On the client end, the callback gets a RemoteReference to that instance. The client can then invoke two's .remote_three() method.
You can use this technique to provide access to arbitrary sets of objects. Just remember that any object that might get passed "over the wire" must inherit from Referenceable (or one of the other flavors). If you try to pass a non-Referenceable object (say, by returning one from a remote_* method), you'll get an InsecureJelly exception. [2]
References can come back to you
If your server gives a reference to a client, and then that client gives the reference back to the server, the server will wind up with the same object it gave out originally. The serialization layer watches for returning reference identifiers and turns them into actual objects. You need to stay aware of where the object lives: if it is on your side, you do actual method calls. If it is on the other side, you do .callRemote(). [3].
#! /usr/bin/python from twisted.spread import pb import twisted.internet.app class Two(pb.Referenceable): def remote_print(self, arg): print "two.print was given", arg class One(pb.Root): def __init__(self, two): #pb.Root.__init__(self) # pb.Root doesn't implement __init__ self.two = two def remote_getTwo(self): print "One.getTwo(), returning my two called", two return two def remote_checkTwo(self, newtwo): print "One.checkTwo(): comparing my two", self.two print "One.checkTwo(): against your two", newtwo if two == newtwo: print "One.checkTwo(): our twos are the same" app = twisted.internet.app.Application("pb3server") two = Two() root_obj = One(two) app.listenTCP(8800, pb.BrokerFactory(root_obj)) app.run()
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor def main(): foo = Foo() pb.getObjectAt("localhost", 8800, 30).addCallback(foo.step1) reactor.run() # keeping globals around is starting to get ugly, so we use a simple class # instead. Instead of hooking one function to the next, we hook one method # to the next. class Foo: def __init__(self): self.oneRef = None def step1(self, obj): print "got one object:", obj self.oneRef = obj print "asking it to getTwo" self.oneRef.callRemote("getTwo").addCallback(self.step2) def step2(self, two): print "got two object:", two print "giving it back to one" print "one is", self.oneRef self.oneRef.callRemote("checkTwo", two) main()
The server gives a Two() instance to the client, who then returns the reference back to the server. The server compares the "two" given with the "two" received and shows that they are the same, and that both are real objects instead of remote references.
A few other techniques are demonstrated in pb3client.py. One is that the callbacks are are added with .addCallback instead of .addCallbacks. As you can tell from the Deferred documentation, .addCallback is a simplified form which only adds a success callback. The other is that to keep track of state from one callback to the next (the remote reference to the main One() object), we create a simple class, store the reference in an instance thereof, and point the callbacks at a sequence of bound methods. This is a convenient way to encapsulate a state machine. Each response kicks off the next method, and any data that needs to be carried from one state to the next can simply be saved as an attribute of the object.
Remember that the client can give you back any remote reference you've given them. Don't base your zillion-dollar stock-trading clearinghouse server on the idea that you trust the client to give you back the right reference. The security model inherent in PB means that they can only give you back a reference that you've given them for the current connection (not one you've given to someone else instead, nor one you gave them last time before the TCP session went down, nor one you haven't yet given to the client), but just like with URLs and HTTP cookies, the particular reference they give you is entirely under their control.
References to client-side objects
Anything that's Referenceable can get passed across the wire, in either direction. The "client" can give a reference to the "server", and then the server can use .callRemote() to invoke methods on the client end. This fuzzes the distinction between "client" and "server": the only real difference is who initiates the original TCP connection; after that it's all symmetric.
#! /usr/bin/python from twisted.spread import pb import twisted.internet.app class One(pb.Root): def remote_takeTwo(self, two): print "received a Two called", two print "telling it to print(12)" two.callRemote("print", 12) app = twisted.internet.app.Application("pb35server") app.listenTCP(8800, pb.BrokerFactory(One())) app.run()
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor class Two(pb.Referenceable): def remote_print(self, arg): print "Two.print() called with", arg def main(): two = Two() def1 = pb.getObjectAt("localhost", 8800, 30) def1.addCallback(got_obj, two) # hands our 'two' to the callback reactor.run() def got_obj(obj, two): print "got One:", obj print "giving it our two" obj.callRemote("takeTwo", two) main()
In this example, the client gives a reference to its own object to the server. The server then invokes a remote method on the client-side object.
Beyond a single "root object": getting objects by name
(This section looks into the code in a way that might help you solidify your understanding of what's going on under the hood, or it might just serve to confuse you horribly. The example described is certainly not the recommended way to accomplish its goal: it depends upon internal interfaces that aren't likely to remain stable, and there are far better ways of doing the same thing. No one will think badly of you if you skip it until later. But I found it to be a useful exercise.)
Before delving into authentication and twisted.cred, it may be useful to look more deeply into the basic PB calls by adding a second object to our server. In practical applications this isn't very useful: it makes more sense to have your root object export a method that can be used to get access to as many other objects as you like. But sometimes doing it the hard way is useful to learn how the code works, and to gain appreciation for the easy way.
On the server side, we want to wind up with an object that acts like pb.BrokerFactory, but which can take extra objects beyond the normal "root" one. On the client side, we want to add an extra argument to the getObjectAt() call, to indicate which of these objects we want to retrieve. As you might guess, this involves subclassing pb.BrokerFactory, and writing a new version of getObjectAt.
How do pb.Broker and pb.BrokerFactory work? Well, pb.Broker objects (which are Protocols, so they are created by a Factory when a new connection is accepted) have a dict that maps object names to the objects themselves. The objects are registerd with pb.Broker's .setNameForLocal method, which accepts a name and the corresponding object.
pb.BrokerFactory objects have an attribute named .objectToBroker. When the connection is accepted and the factory is asked to do .buildProtocol, it creates the Broker(), does some setup, and then binds "root" to its .objectToBroker object by calling .setNameForLocal() in the new broker. [5]
So, we need a BrokerFactory that will do multiple .setNameForLocal()s on the Brokers that it creates. In the example, we subclass BrokerFactory to take an extra dictionary of names and objects that should be put into the resulting Broker.
#! /usr/bin/python from twisted.spread import pb import twisted.internet.app class Echo1(pb.Root): def remote_foo(self, arg): print "Echo1.foo() got:", arg class EchoN(pb.Referenceable): def __init__(self, which): #pb.Referenceable.__init__(self) self.which = which def remote_foo(self, arg): print "EchoN[%d].foo() got:" % self.which, arg class MyBrokerFactory(pb.BrokerFactory): def __init__(self, objectToBroker, objectDict): pb.BrokerFactory.__init__(self, objectToBroker) self.objects = objectDict def buildProtocol(self, addr): proto = pb.BrokerFactory.buildProtocol(self, addr) # that added the "root" object. Now lets add the others. for name in self.objects.keys(): proto.setNameForLocal(name, self.objects[name]) return proto app = twisted.internet.app.Application("pb4server") rootobject = Echo1() objects = { "one": EchoN(1), "two": EchoN(2), "three": EchoN(3), } app.listenTCP(8800, MyBrokerFactory(rootobject, objects)) app.run()
Now, what about the client? To make this work, we have to look into the implementation of the convenience function getObjectAt(). As you learned in the 'writing twisted clients' document, clients are built out of Protocols just like servers are, but those Protocols are created with ClientFactories instead of normal Factory objects. The Protocol that implements the client side of a PB connection is the same Broker class as used on the server side, but it gets an extra flag to indicate it is a client. Let's look at the code behind getObjectAt (stripped down to ignore the UNIX-domain socket case):
def getObjectAt(host, port, timeout=None): d = defer.Deferred() b = Broker(1) bf = BrokerClientFactory(b) _ObjectRetrieval(b, d) reactor.connectTCP(host, port, bf, timeout) return d
_ObjectRetrieval is a small utility class which sets up a number of callbacks. We only care about what happens when the connection is established:
class _ObjectRetrieval: ... def connectionMade(self): x = self.broker.remoteForName("root") del self.broker self.term = 1 self.deferred.callback(x)
When the connection is established, the Broker object is asked to retrieve the remote object indexed by the name "root". Once this is retrieved, the callback is run, passing the remote object reference as an argument. That callback is the one hooked to the Deferred that was passed back from getObjectAt().
So, to get something other than "root", we're going to need to replace getObjectAt() with a version that will take an object name, and then have it use a subclass of _ObjectRetrieval which can request that new name. These two functions use some internal symbols, so our versions will have to import more than a simple user of the normal behavior would need.
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor def main(): rootobj_def = pb.getObjectAt("localhost", 8800, 30) rootobj_def.addCallbacks(got_rootobj) obj2_def = getSomeObjectAt("localhost", 8800, 30, "two") obj2_def.addCallbacks(got_obj2) obj3_def = getSomeObjectAt("localhost", 8800, 30, "three") obj3_def.addCallbacks(got_obj3) reactor.run() def got_rootobj(rootobj): print "got root object:", rootobj print "telling root object to do foo(A)" rootobj.callRemote("foo", "A") def got_obj2(obj2): print "got second object:", obj2 print "telling second object to do foo(B)" obj2.callRemote("foo", "B") def got_obj3(obj3): print "got third object:", obj3 print "telling third object to do foo(C)" obj3.callRemote("foo", "C") class my_ObjectRetrieval(pb._ObjectRetrieval): def __init__(self, broker, d, objname): pb._ObjectRetrieval.__init__(self, broker, d) self.objname = objname def connectionMade(self): assert not self.term, "How did this get called?" x = self.broker.remoteForName(self.objname) del self.broker self.term = 1 self.deferred.callback(x) def getSomeObjectAt(host, port, timeout=None, objname="root"): from twisted.internet import defer from twisted.spread.pb import Broker, BrokerClientFactory d = defer.Deferred() b = Broker(1) bf = BrokerClientFactory(b) my_ObjectRetrieval(b, d, objname) if host == "unix": # every time you use this, God kills a kitten reactor.connectUNIX(port, bf, timeout) else: reactor.connectTCP(host, port, bf, timeout) return d main()
Run pb4server.py, then run pb4client.py from a different shell. Note that the different objects get the right arguments. Also note that they probably aren't run in the same order as the getSomeObjectAt() functions were executed. Welcome to the network.
flavors of remotely-accessible objects: Referenceable and siblings
This is explained better in pb.html, in the section entitled Things you can Call Remotely
twisted/spread/flavors.py defines an abstract base class called Serializable. There are four subclasses of Serializeable defined there: Referenceable, Viewable, Copyable, and RemoteCache. These styles are copied over into twisted/spread/pb.py, and are usually accessed as pb.Referenceable, etc. For a given class to be remotely referenceable (i.e. to allow references to it to get passed across the wire), that class must inherit from one of these styles. Basic types (numbers, dictionaries, etc) are always serializable, but classes must declare which style they want by subclassing one.
The style of serialization defined by those four classes influences how references to the objects get transferred over the wire.
- pb.Referenceable is the most straightforward. It does not provide any access to the data attributes of the object, but allows .callRemote() to be used to invoke any 'remote_*' methods that exist. When one of these is sent over the wire, it is simply serialized with an identifier that can be returned in the .callRemote() request.
- pb.Viewable much like Referenceable, but instead of exporting 'remote_*' methods, objects derived from this class export 'view_*' methods. These methods are invoked with an extra first argument that contains the Perspective which provided the reference to the original pb.Viewable object.
- pb.Copyable: references to pb.Copyable-derived objects are transferred over the wire by copying them wholesale. The default behavior (which can of course be overridden) is to send the name of the object's class as a string, and a dict with all the attribute. The receiving end must have a class that derives from pb.RemoteCopy, that is registered to be created when the named class is sent over the wire. Be careful with the implementation of this class, as the server has control of what data is used in creating those objects, and you must make sure the server cannot trick the client into doing something naughty with that data.
- pb.Cacheable: this is derived from pb.Copyable, and attempts to make sure the data is only copied once. There is also code in place to notify the client when the state of the object changes. flavors.py says this is the most complex and least-documented part of PB. Good luck.
[[[[ Footnote [h4]: When a perspective_* method returns a pb.Viewable object, an intermediate object called a ViewPoint is created, and the client gets a RemoteReference to that ViewPoint instead. The ViewPoint remembers which Perspective it came from. Running .callRemote() on the reference of the ViewPoint results in a 'view_*' method being invoked on the pb.Viewable object. ]]]]
[[[[ Footnote [h4]: Note that it is possible to have both 'perspective_*' methods and 'view_*' methods in the same class. See the docstring for spread.flavors.ViewPoint for details. ]]]]
This is useful to let the object know which client is calling the method. This will make more sense after you've read through the pb.cred section below.
So those are the four kinds of remotely-referenceable objects. There are also multiple kinds of remote method calls, somewhat loosely linked to those four classes (now known as the Serializer Siblings) (except they're all subclasses of Serializer) (and Serializer Subclasses sounds so stupid). Ahem. The differences between the remote method calls depends upon what your RemoteReference is actually pointing to on the other side.
If it is pointing to a simple pb.Referenceable, then your .callRemote("foo") gets turned into a .remote_foo() on the other end.
If it points at a Perspective (described below), then .callRemote("foo") invokes .perspective_foo() on that object.
If your Perspective named perspective1 returns a reference to a pb.Viewable named thing1, then .callRemote("foo", arg1) on the resulting RemoteReference causes thing1.view_foo(perspective1, arg1) to be invoked. This lets thing1 tell the difference between perspective1 doing .view_foo() and some other perspective doing the same method. pb.Referenceable subclasses have no way to find out who is invoking their methods, but pb.Viewable derivatives can.
Again, Perspectives will make much more sense after the next section. My advice is to pronouce Perspective as u-s-e-r until you've read that part.
Wrapup before touching twisted.cred
So now you've got all of PB at your feet (except the P part, that's waiting for the next chapter). Once you architect your system around the idea of objects in different processes invoking methods on each other, then it's a simple matter of writing the right pb.Referenceable and pb.Root classes, adding 'remote_*' methods to them, and starting the clients with .getObjectAt(). Remember that .callRemote() always returns a Deferred, so think asynchronously: state machines are common, as are series of chained callbacks to implement multi-step RPC sequences one step at a time.
It would be a good idea to pause now and write some code to get the feel for PB (but for the sake of the exercise, stay away from the pb.Perspective class: stick to pb.Root and pb.Referenceable). Try writing a small chat server: The server is the initial pb.Root object, each user gets their own object, each group has its own object, the Users have a list of Groups they belong to, the Groups have a list of Users who have joined them. Users can send a message to a Group; the Group then sends the message out to all their Users. The client side can probably be implemented with a single remote_showText() method, invokable by the Groups on the other end. For this exercise, ignore security completely. When the User first connects, let them tell you what their name is, and give them a reference to their User object based upon that name.
The two reasons for writing this code now, before you go on to the next section: one: it will solidify your understanding of basic PB remote method calls without adding in the confusion of authentication yet, and two: you will probably encounter some common problems that will help you understand why twisted.cred provides the facilities that it does.
Implementing a chat server is a doubly useful exercise, because twisted.words provides a very similar service. When you've finished with the next section on twisted.cred, you'll see how the differences between your code and the implementation in twisted.words is mainly in the authorization and user/perspective areas. (well, if you ignore the web interface, the IRC interface, and all the hooks for running bots of various kinds).
Authentication, Identities, and Perspectives
This next section is an exciting ride. Keep your arms and legs inside the car at all times, you must be *this* tall to get on board, and we cannot be responsible for any lost or stolen articles. Permanecen sentados, por favor.
Motivation
So what we've got so far is a really clean way to get objects and references thrown around from one process to another. You've written a chat server, and see the power of easy remote method calls.
But there were some problems. You had to trust the user when they said their name was 'bob': no passwords or anything. If you wanted a direct-send one-to-one message feature, you might have implemented it by handing a User reference directly off to another User. (so they could invoke .remote_sendMessage() on the receiving User): but that lets them do anything else to that user too, things that should probably be restricted to the "owner" user, like .remote_joinGroup() or .remote_quit().
And there were probably places where the easiest implementation was to have the client send a message that included their own name as an argument. Sending a message to the group could just be:
class Group(pb.Referenceable): ... def remote_sendMessage(self, from_user, message): for user in self.users: user.callRemote("sendMessage", "<%s>: %s" % (from_user, message))
But obviously this lets users spoof each other: there's no reason that Alice couldn't do:
remotegroup.callRemote("sendMessage", "bob", "i like pork")
much to the horror of Bob's vegetarian friends.
(In general, learn to get suspicious if you see "groupName" or "userName" in an argument list).
You could fix this by adding more classes (with fewer remotely-invokable methods), and making sure that the reference you give to Alice won't let her pretend to be anybody else. You'd probably give Alice an object that had her name buried inside:
class User(pb.Referenceable): def __init__(self, name): self.name = name def remote_sendMessage(self, group, message): g = findgroup(group) for user in g.users: user.callRemote("sendMessage", "<%s>: %s" % (self.name, message)
But now she could sneak into another group. So you might have to have an object per-group-per-user:
class UserGroup(pb.Referenceable): def __init__(self, group, user): self.group = group self.user = user def remote_sendMessage(self, message): name = self.user.name for user in self.group.users: user.callRemote("sendMessage", "<%s>: %s" % (name, message)
But that means more code, and more code is bad, especially when it's a common problem (everybody designs with security in mind, right? Right??). [10]
So we have a security problem. We need a way to ask for and verify a password, so we know that Bob is really Bob and not Alice wearing her "Hi, my name is Bob" t-shirt. And it would make the code cleaner (read: fewer classes) if some methods could know *who* is calling them. You could add that layer of password checking to your application, but once again the basement denizens at Twisted Matrix Laboratories have beaten you to it.
A sample application
As a framework for this chapter, we'll be referring to a hypothetical game implemented by several programs using the Twisted framework. This game is sort of a MUD-like thing, where users log in using their client programs, and there is a server, and users can do some things but not others. [11]
Let's say the players make moves in this game by invoking remote methods on objects that live in the server. The clients can't really be relied upon to tell the server who they are with each move they make: they might get it wrong, or (horrors!) lie to mess up the other player.
Actually, let's simplify it to a server-based game of Go (if that can be considered simple). Go has two players, white and black, who take turns placing stones of their own color at the intersections of a 19x19 grid. If we represent the game and board as an object in the server called Game, then the players might interact with it using something like this:
class Game(pb.Referenceable): def remote_getBoard(self): return self.board # a dict, with the state of the board def remote_move(self, playerName, x, y): self.board[x,y] = playerName
"But Wait", you say, yes that method takes a playerName, which means they could cheat and move for the other player. So instead, do this:
class Game(pb.Referenceable): def remote_getBoard(self): return self.board # a dict, with the state of the board def move(self, playerName, x, y): self.board[x,y] = playerName
and move the responsibility (and capability) for calling Game.move() out to a different class. That class is a pb.Perspective.
Perspectives
pb.Perspective (and some related classes: Identity, Authorizer, and Service) is a layer on top of the basic PB system that handles username/password checking. The basic idea is that there is a Perspective object (probably a subclass you've created) for each user [12], and only the authorized user gets a remote reference to that Perspective object. Your code can then look like this:
class Game: def getBoard(self): return self.board # a dict, with the state of the board def move(self, playerName, x, y): self.board[x,y] = playerName class PlayerPerspective(pb.Perspective): def __init__(self, playerName, game): self.playerName = playerName self.game = game def perspective_move(self, x, y): self.game.move(self.playerName, x, y) def perspective_getBoard(self): return self.game.getBoard()
The code on the server side creates the PlayerPerspective object, giving it the right playerName and a reference to the Game object. The remote player doesn't get a reference to the Game object, only their own PlayerPerspective, so they don't have an opportunity to lie about their name: it comes from the .playerName attribute, not an argument of their remote method call.
Here is a brief example of using a Perspective. Most of the support code is magic for now: we'll explain it later. [13]
#! /usr/bin/python from twisted.spread import pb from twisted.cred.authorizer import DefaultAuthorizer import twisted.internet.app class MyPerspective(pb.Perspective): def perspective_foo(self, arg): print "I am", self.myname, "perspective_foo(",arg,") called on", self # much of the following is magic app = twisted.internet.app.Application("pb5server") auth = DefaultAuthorizer(app) # create the service, tell it to generate MyPerspective objects when asked s = pb.Service("myservice", app, auth) s.perspectiveClass = MyPerspective # create a MyPerspective p1 = s.createPerspective("perspective1") p1.myname = "p1" # create an Identity, give it a name and password, and allow it access to # the MyPerspective we created before i1 = auth.createIdentity("user1") i1.setPassword("pass1") i1.addKeyByString("myservice", "perspective1") auth.addIdentity(i1) # start the application app.listenTCP(8800, pb.BrokerFactory(pb.AuthRoot(auth))) app.run()
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor def main(): def1 = pb.connect("localhost", 8800, "user1", "pass1", "myservice", "perspective1", 30) def1.addCallbacks(connected) reactor.run() def connected(perspective): print "got perspective ref:", perspective print "asking it to foo(12)" perspective.callRemote("foo", 12) main()
Note that once this example has done the method call, you'll have to terminate both ends yourself. Also note that the Perspective's .attached() and .detached() methods are run when the client connects and disconnects. The base class implementations of these methods just prints a message.
Ok, so that wasn't really very exciting. It doesn't accomplish much more than the first PB example, and used a lot more code to do it. Let's try it again with two users this time, each with their own Perspective. We also override .attached() and .detached(), just to see how they are called.
[[[Footnote: the 'clientref' argument to .attached will hold a remote reference to an object provided by the client, in the pb.connect's optional argument 'client'. The server-side Perspective can use it to do remote methods on something in the client, so that the client doesn't always have to drive the interaction. In a chat server, the client object would be the one to which "display text" messages were sent. In a game, this would provide a way to tell the clients that someone has made a move, so they can update their game boards. Note: to actually use it, you'd probably want to subclass Perspective and change the .attached method to stash the clientref somewhere.]]]
[[[Footnote: 'attached' has the opportunity to return a different Perspective, if it so chooses, based upon something in the Identity. The client will get a reference to whatever .attached() returns, so the default case is to 'return self'.]]]
[[[Footnote: 'detached' will be called with the same 'clientref' object that was given to the original 'attached' call.]]]
#! /usr/bin/python from twisted.spread import pb from twisted.cred.authorizer import DefaultAuthorizer import twisted.internet.app class MyPerspective(pb.Perspective): def attached(self, clientref, identity): print "client attached! they are:", identity return self def detached(self, ref, identity): print "client detached! they were:", identity def perspective_foo(self, arg): print "I am", self.myname, "perspective_foo(",arg,") called on", self # much of the following is magic app = twisted.internet.app.Application("pb5server") auth = DefaultAuthorizer(app) # create the service, tell it to generate MyPerspective objects when asked s = pb.Service("myservice", app, auth) s.perspectiveClass = MyPerspective # create one MyPerspective p1 = s.createPerspective("perspective1") p1.myname = "p1" # create an Identity, give it a name and password, and allow it access to # the MyPerspective we created before i1 = auth.createIdentity("user1") i1.setPassword("pass1") i1.addKeyByString("myservice", "perspective1") auth.addIdentity(i1) # create another MyPerspective p2 = s.createPerspective("perspective2") p2.myname = "p2" i2 = auth.createIdentity("user2") i2.setPassword("pass2") i2.addKeyByString("myservice", "perspective2") auth.addIdentity(i2) # start the application app.listenTCP(8800, pb.BrokerFactory(pb.AuthRoot(auth))) app.run()
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor def main(): def1 = pb.connect("localhost", 8800, "user1", "pass1", "myservice", "perspective1", 30) def1.addCallbacks(connected) reactor.run() def connected(perspective): print "got perspective1 ref:", perspective print "asking it to foo(13)" perspective.callRemote("foo", 13) main()
#! /usr/bin/python from twisted.spread import pb from twisted.internet import reactor def main(): def1 = pb.connect("localhost", 8800, "user2", "pass2", "myservice", "perspective2", 30) def1.addCallbacks(connected) reactor.run() def connected(perspective): print "got perspective2 ref:", perspective print "asking it to foo(14)" perspective.callRemote("foo", 14) main()
While pb6server.py is running, try starting pb6client1, then pb6client2. Compare the argument passed by the .callRemote() in each client. You can see how each client logs into a different Perspective.
Class Overview
Now that we've seen some of the motivation behind the Perspective class, let's start to de-mystify some of the parts labeled "magic" in pb6server.py. Here are the major classes involved:
Application
Service
Authorizer
Identity
Perspective
You've already seen Application
. It holds the program-wide
settings, like which uid/gid it should run under, and contains a list of
ports that it should listen on (with a Factory for each one to create
Protocol objects). When used for PB, we put a pb.BrokerFactory on the
port.
A Service
is, well, a service. A web server would be a
Service
, as would a chat server, or any other kind of server
you might choose to run. What's the difference between a
Service
and an Application
? You can have multiple
Service
s in a single Application
: perhaps both a
web-based chat service and an IM server in the same program, that let you
exchange message between the two. Or your program might provide different
kinds of interfaces to different classes of users: administrators could get
one Service
, while mere end-users get a less-powerful
Service
.
[[[Footnote: Note that the Service
is a server of some sort,
but that doesn't mean there's a one-to-one relationship between the
Service
and the TCP port that's being listened to. Several
different Service
s can hang off the same TCP port. Look at the
MultiService class for details.]]]
The Service
is reponsible for providing
Perspective
objects. More on that later.
The Authorizer
is a class that provides
Identity
objects. The abstract base class is
twisted.cred.authorizer.Authorizer, and for simple purposes you can just use
DefaultAuthorizer, which is a subclass that stores pre-generated Identities
in a simple dict (indexed by username). The Authorizer
's
purpose in life is to implement the .getIdentityRequest() method, which
takes a user name and (eventually) returns the corresponding
Identity
object.
Each Identity
object represents a single user, with a
username and a password of some sort. Its job is to talk to the
as-yet-anonymous remote user and verify that they really are who they claim
to be. The default twisted.cred.authorizer.Identity
class implements MD5-hashed challenge-response password authorization, much
like the HTTP MD5-Authentication method: the server sends a random challenge
string, the client concatenates a hash of their password with the challenge
string, and sends back a hash of the result. At this point the client is
said to be "authorized" for access to that Identity
, and they
are given a remote reference to the Identity
(actually a
wrapper around it), giving them all the privileges of that
Identity
.
Those privileges are limited to requesting Perspective
s. The
Identity
object also has a "keyring", which is a list of
(serviceName, perspectiveName) pairs that the corresponding authorized user
is allowed to access. Once the user has been authenticated, the
Identity
's job is to implement .requestPerspectiveForKey(),
which it does by verifying the "key" exists on the keyring, then asking the
matching Service
to do .getPerspectiveForIdentity().
Finally, the Perspective
is the subclass of pb.Perspective
that implements whatever 'perspective_*' methods you wish to expose to an
authenticated remote user. It also implements .attached() and .detached(),
which are run when the user connects (actually when they finish the
authentication sequence) or disconnects.
Class Responsibilities
Now that we've gone over the classes and objects involved, let's look at the specific responsibilities of each. Most of these classes are on the hook to implement just one or two particular methods, and the rest of the class is just support code (or the main method has been broken up for ease of subclassing). This section indicates what those main methods are and when they get called.
The Authorizer
has to provide Identity
objects
(requested by name) by implementing .getIdentityRequest(). The
DefaultAuthorizer class just looks up the name in a dict called
self.identities, so when you use it, you have to make the Identities ahead
of time (using i = auth.createIdentity()
) and
store them in that dict (by giving them to auth.addIdentity(i)
).
However, you can make a subclass of Authorizer
with a
.getIdentityRequest method that behaves differently: your version could look
in /etc/passwd, or do an SQL database lookup [[[Footnote: see
twisted.enterprise.dbcred for a module that does exactly that]]], or create
new Identities for anyone that asks (with a really secret password like
'1234' that the user will probably never change, even if you ask them to).
The Identities could be created by your server at startup time and stored in
a dict, or they could be pickled and stored in a file until needed (in which
case .getIdentityRequest() would use the username to find a file, unpickle
the contents, and return the Identity
object that results), or
created brand-new based upon whatever data you want. Any function that
returns a Deferred (that will eventually get called back with the
Identity
object) can be used here.
[[[Footnote: for static Identities that are available right away, the Deferred's callback() method is called right away. This is why the interface of .getIdentityRequest() specifies that its Deferred is returned unarmed, so that the caller has a chance to actually add a callback to it before the callback gets run.]]]
The Identity
object thus returned has two responsibilities.
The first is to authenticate the user. It does this by implementing
.verifyPassword, which is called by IdentityWrapper (wait for it) as part of
the challenge-response sequence. The second is to provide
Perspective
s: the authenticated user provides a service name
and a perspective name, and .requestPerspectiveForKey() is invoked to
retrieve the given Perspective
. You could subclass
Identity
to change the behavior of either of these, but chances
are you won't bother. The only reason to change .verifyPassword() would be
to replace it with some kind of public-key verification scheme (and that
would require changes to pb.IdentityWrapper too). Any changes you might want
to make to .requestPerspectiveForKey() are probably more appropriate to put
in the Service's .getPerspectiveForIdentity instead.
The Service object's job is to provide Perspective
s, by
implementing .getPerspectiveForIdentity(). The default implementation (in
twisted.spread.pb.Service) retrieves static pre-generated
Perspective
s from a dict (indexed by perspective name), much
like DefaultAuthorizer does with Identities. And like
Authorizer
, it is very useful to subclass pb.Service to change
the way .getPerspectiveForIdentity works: to create
Perspective
s out of persistent data or database lookups, to set
extra attributes in the Perspective
, etc.
In the default implementation, you need to create the
Perspective
s at startup time, by calling .createPerspective().
This uses an attribute of the Service object named .perspectiveClass when
creating the objects, so you can simply change that to have it build your
own Perspective
-derivatives instead of the default type.
How that example worked
Ok, so that's what everything is supposed to do. Now you can walk through
the previous example and see what was going on: we created a subclass called
MyPerspective, made a DefaultAuthorizer and added it to the
Application
, created a Service and told it to make
MyPerspectives, used .createPerspective() to build a few, for each one we
made an Identity
(with a username and password), and allowed
that Identity
to access a single MyPerspective by adding it to
the keyring. We added the Identities to the Authorizer
, and
then glued the authorizer to the pb.BrokerFactory.
How did that last bit of magic glue work? I won't tell you here
[[[Footnote: but I will tell you here:app.listenTCP(8800, pb.BrokerFactory(pb.AuthRoot(auth)))
pb.AuthRoot() provides objects that are subclassed from pb.Root, so as we
saw in the first example, they can be served up by pb.BrokerFactory().
AuthRoot happens to use the .rootObject hook described earlier to serve up
an AuthServ object, which wraps the Authorizer
and offers a
method called .remote_username, which is called by the client to declare
which Identity
it claims to be. That method starts the
challenge-response sequence.
, because it isn't very useful to override it, but you effectively hang
an Authorizer
off of a TCP port. The combination of the object
and methods exported by the pb.AuthRoot object works together with the code
inside the pb.connect() function to implement both sides of the
challenge-response sequence. When you (as the client) use pb.connect() to
get to a given host/port, you end up talking to a single
Authorizer
. The username/password you give get matched against
the Identities provided by that authorizer, and then the
servicename/perspectivename you give are matched against the ones authorized
by the Identity
(in its .keyring attribute). You eventually get
back a remote reference to a Perspective provided by the Service that you
named.
Walkthrough
So, now that you've seen the complete sequence, it's time for some code walkthrough. This will give you a chance to see the places where you might write subclasses to implement different behaviors. We will look at what happens when pb6client1.py meets pb6server.py. We tune in just as the client has run the pb.connect() call.
The client-side code can be summarized by the following sequence of function calls, all implemented in twisted/spread/pb.py . pb.connect() calls getObjectAt() directly, afterwards each step is executed as a callback when the previous step completes.
getObjectAt(host,port,timeout) logIn(): authServRef.callRemote('username', username) _cbLogInRespond(): challenger.callRemote('respond', f[challenge,password]) _cbLogInResponded(): identity.callRemote('attach', servicename, perspectivename, client) usercallback(perspective)
The client does getObjectAt() to connect to the given host and port, and retrieve the object named "root". On the server side, the BrokerFactory accepts the connection, asks the pb.AuthRoot() object for its .rootObject(), getting an AuthServ() object (containing both the authorizer and the Broker protocol object). It gives a remote reference to that AuthServ out to the client.
Now the client invokes the 'remote_username' method on that AuthServ. The
AuthServ asks the Authorizer
to .getIdentityRequest: this
retrieves (or creates) the Identity
. When that finishes, it
asks the Identity
to create a random challenge (usually just a
random string). The client is given back both the challenge and a reference
to a new AuthChallenger object which will only accept a response that
matches that exact challenge.
The client does its part of the MD5 challenge-response protocol and sends
the response to the AuthChallenger's .remote_response() method. The
AuthChallenger verifies the response: if it is valid then it gives back a
reference to an IdentityWrapper, which contains an internal reference to the
Identity
that we now know matches the user at the other end of
the connection.
The client then invokes the 'remote_attach' method on that
IdentityWrapper, passing in a serviceName, perspectiveName, and remoteRef.
The wrapper asks the Identity
to get a perspective using
identity.requestPerspectiveForKey, which does the "is this user allowed to
get this service/perspective" check by looking at the tuples on its
.keyring, and if that is allowed then it gets the Service (by giving
serviceName to the authorizer), then asks the Service to provide the
perspective (with svc.getPerspectiveForIdentity). The default Service will
ignore the identity object and just look for Perspectives by
perspectiveName. The Service looks up or creates the Perspective and returns
it. The remote_attach method runs the Perspective's .attached method
(although there are some intermediate steps, in IdentityWrapper._attached,
to make sure .detached will eventually be run, and the Perspective's
.brokerAttached method is executed to give it a chance to return some other
Perspective instead). Finally a remote reference to the Perspective is
returned to the client.
The client gives the Perspective reference to the callback that was attached to the Deferred that pb.connect() returned, which brings us back up to the code visible in pb6client1.py.
That's the end of the tour. If you have any questions, the folks at the welcome office will be more than happy to help. Don't forget to stop at the gift store on your way out, and have a really nice day. Buh-bye now!
Footnotes
[1]There are a few other classes that can bestow this ability, but Referenceable is the easiest to understand; see 'flavors' below for details on the others.
[2]Footnote: this can be overridden, by subclassing one of the Serializable flavors and defining custom serialization code for your class. See XXX for details.
[3]Footnote: The binary nature of this local vs. remote scheme works because you cannot give RemoteReferences to a third party. If you could, then your object A could go to B, B could give it to C, C might give it back to you, and you would be hard pressed to tell if the object lived in C's memory space, in B's, or if it was really your own object, tarnished and sullied after being handed down like a really ugly picture that your great aunt owned and which nobody wants but which nobody can bear to throw out. Ok, not really like that, but you get the idea.
[5]Footnote [h4] Actually, it gives the .objectToBroker a chance to substitute a different object in its place, by calling the .rootObject() method:
proto.setNameForLocal("root",
self.objectToBroker.rootObject(proto))
However, the default behavior of Root.rootObject() is to simply "return self".
This hook exists so that subclasses of Root can return different objects depending upon something in the Broker (which is a Protocol) being used. Since there is one Broker per connection, and the connection source address is stored in it, the Root-ish object could give out different objects depending upon which address is connecting.
pb.AuthRoot (subclassed from pb.Root) uses this to return an AuthServ object, which starts the challenge-response authentication sequence described in detail in the section on pb.cred.
[10]Footnote: third party references
Note that the reference that the server gives to the client is only useable by that one client: if it tries to hand it off to a third party, they'll get an exception (XXX: which?). This appears to help: only the client you gave the reference to can cause any damage with it. Of course, the client might agree to do anything the third party wants. If you don't trust them, don't give them that reference.
Also note that the design of the serialization mechanism (XXX: pb.jelly?) makes it impossible for the client to obtain a reference that they weren't explicitly given. References passed over the wire are given id numbers and recorded in a per-connection dictionary. If you didn't give them the reference, the id number won't be in the dict, and no amount of id guessing by a malicious client will give them anything else.
[11]There actually exists such a thing. It's called twisted.reality, and was the whole reason Twisted was created. I haven't played it yet: I'm too afraid
[12]actually there is a perspective per user*service, but we'll get into that later
[13]This example has more support code than you'd actually need. If you only have one Service, then there's probably a one-to-one relationship between your Identities and your Perspectives. If that's the case, you can use a utility method called Perspective.makeIdentity() instead of creating the perspectives and identities in separate steps. This is shorter, but hides some of the details that are useful here to explain what's going on. Again, this will make more sense later.
$Id: pb.html,v 1.13 2002/10/04 07:03:24 warner Exp $
Brian Warner <warner@lothar.com> Last modified: Thu Oct 3 23:55:42 PDT 2002