root / trunk / twisted / protocols / amp.py

Revision 26697, 75.4 kB (checked in by exarkun, 3 months ago)

Merge amp-repr-3695

Author: SeanMcQ, exarkun
Reviewer: exarkun, glyph
Fixes: #3695

Change the repr of twisted.protocols.amp.AMP instances to exclude the
inner protocol if one does not exist and to be more explicit about what
it is if one does exist.

Line 
1 # -*- test-case-name: twisted.test.test_amp -*-
2 # Copyright (c) 2005 Divmod, Inc.
3 # Copyright (c) 2007-2009 Twisted Matrix Laboratories.
4 # See LICENSE for details.
5
6 """
7 This module implements AMP, the Asynchronous Messaging Protocol.
8
9 AMP is a protocol for sending multiple asynchronous request/response pairs over
10 the same connection.  Requests and responses are both collections of key/value
11 pairs.
12
13 AMP is a very simple protocol which is not an application.  This module is a
14 "protocol construction kit" of sorts; it attempts to be the simplest wire-level
15 implementation of Deferreds.  AMP provides the following base-level features:
16
17     - Asynchronous request/response handling (hence the name)
18
19     - Requests and responses are both key/value pairs
20
21     - Binary transfer of all data: all data is length-prefixed.  Your
22       application will never need to worry about quoting.
23
24     - Command dispatching (like HTTP Verbs): the protocol is extensible, and
25       multiple AMP sub-protocols can be grouped together easily.
26
27 The protocol implementation also provides a few additional features which are
28 not part of the core wire protocol, but are nevertheless very useful:
29
30     - Tight TLS integration, with an included StartTLS command.
31
32     - Handshaking to other protocols: because AMP has well-defined message
33       boundaries and maintains all incoming and outgoing requests for you, you
34       can start a connection over AMP and then switch to another protocol.
35       This makes it ideal for firewall-traversal applications where you may
36       have only one forwarded port but multiple applications that want to use
37       it.
38
39 Using AMP with Twisted is simple.  Each message is a command, with a response.
40 You begin by defining a command type.  Commands specify their input and output
41 in terms of the types that they expect to see in the request and response
42 key-value pairs.  Here's an example of a command that adds two integers, 'a'
43 and 'b'::
44
45     class Sum(amp.Command):
46         arguments = [('a', amp.Integer()),
47                      ('b', amp.Integer())]
48         response = [('total', amp.Integer())]
49
50 Once you have specified a command, you need to make it part of a protocol, and
51 define a responder for it.  Here's a 'JustSum' protocol that includes a
52 responder for our 'Sum' command::
53
54     class JustSum(amp.AMP):
55         def sum(self, a, b):
56             total = a + b
57             print 'Did a sum: %d + %d = %d' % (a, b, total)
58             return {'total': total}
59         Sum.responder(sum)
60
61 Later, when you want to actually do a sum, the following expression will return
62 a L{Deferred} which will fire with the result::
63
64     ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
65         lambda p: p.callRemote(Sum, a=13, b=81)).addCallback(
66             lambda result: result['total'])
67
68 You can also define the propagation of specific errors in AMP.  For example,
69 for the slightly more complicated case of division, we might have to deal with
70 division by zero::
71
72     class Divide(amp.Command):
73         arguments = [('numerator', amp.Integer()),
74                      ('denominator', amp.Integer())]
75         response = [('result', amp.Float())]
76         errors = {ZeroDivisionError: 'ZERO_DIVISION'}
77
78 The 'errors' mapping here tells AMP that if a responder to Divide emits a
79 L{ZeroDivisionError}, then the other side should be informed that an error of
80 the type 'ZERO_DIVISION' has occurred.  Writing a responder which takes
81 advantage of this is very simple - just raise your exception normally::
82
83     class JustDivide(amp.AMP):
84         def divide(self, numerator, denominator):
85             result = numerator / denominator
86             print 'Divided: %d / %d = %d' % (numerator, denominator, total)
87             return {'result': result}
88         Divide.responder(divide)
89
90 On the client side, the errors mapping will be used to determine what the
91 'ZERO_DIVISION' error means, and translated into an asynchronous exception,
92 which can be handled normally as any L{Deferred} would be::
93
94     def trapZero(result):
95         result.trap(ZeroDivisionError)
96         print "Divided by zero: returning INF"
97         return 1e1000
98     ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
99         lambda p: p.callRemote(Divide, numerator=1234,
100                                denominator=0)
101         ).addErrback(trapZero)
102
103 For a complete, runnable example of both of these commands, see the files in
104 the Twisted repository::
105
106     doc/core/examples/ampserver.py
107     doc/core/examples/ampclient.py
108
109 On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and
110 values, and empty keys to separate messages::
111
112     <2-byte length><key><2-byte length><value>
113     <2-byte length><key><2-byte length><value>
114     ...
115     <2-byte length><key><2-byte length><value>
116     <NUL><NUL>                  # Empty Key == End of Message
117
118 And so on.  Because it's tedious to refer to lengths and NULs constantly, the
119 documentation will refer to packets as if they were newline delimited, like
120 so::
121
122     C: _command: sum
123     C: _ask: ef639e5c892ccb54
124     C: a: 13
125     C: b: 81
126
127     S: _answer: ef639e5c892ccb54
128     S: total: 94
129
130 Notes:
131
132 In general, the order of keys is arbitrary.  Specific uses of AMP may impose an
133 ordering requirement, but unless this is specified explicitly, any ordering may
134 be generated and any ordering must be accepted.  This applies to the
135 command-related keys I{_command} and I{_ask} as well as any other keys.
136
137 Values are limited to the maximum encodable size in a 16-bit length, 65535
138 bytes.
139
140 Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes.
141 Note that we still use 2-byte lengths to encode keys.  This small redundancy
142 has several features:
143
144     - If an implementation becomes confused and starts emitting corrupt data,
145       or gets keys confused with values, many common errors will be signalled
146       immediately instead of delivering obviously corrupt packets.
147
148     - A single NUL will separate every key, and a double NUL separates
149       messages.  This provides some redundancy when debugging traffic dumps.
150
151     - NULs will be present at regular intervals along the protocol, providing
152       some padding for otherwise braindead C implementations of the protocol,
153       so that <stdio.h> string functions will see the NUL and stop.
154
155     - This makes it possible to run an AMP server on a port also used by a
156       plain-text protocol, and easily distinguish between non-AMP clients (like
157       web browsers) which issue non-NUL as the first byte, and AMP clients,
158       which always issue NUL as the first byte.
159 """
160
161 __metaclass__ = type
162
163 import types, warnings
164
165 from cStringIO import StringIO
166 from struct import pack
167
168 from zope.interface import Interface, implements
169
170 from twisted.python.compat import set
171 from twisted.python.util import unsignedID
172 from twisted.python.reflect import accumulateClassDict
173 from twisted.python.failure import Failure
174 from twisted.python import log, filepath
175
176 from twisted.internet.main import CONNECTION_LOST
177 from twisted.internet.error import PeerVerifyError, ConnectionLost
178 from twisted.internet.error import ConnectionClosed
179 from twisted.internet.defer import Deferred, maybeDeferred, fail
180 from twisted.protocols.basic import Int16StringReceiver, StatefulStringProtocol
181
182 try:
183     from twisted.internet import ssl
184 except ImportError:
185     ssl = None
186
187 if ssl and not ssl.supported:
188     ssl = None
189
190 if ssl is not None:
191     from twisted.internet.ssl import CertificateOptions, Certificate, DN, KeyPair
192
193 ASK = '_ask'
194 ANSWER = '_answer'
195 COMMAND = '_command'
196 ERROR = '_error'
197 ERROR_CODE = '_error_code'
198 ERROR_DESCRIPTION = '_error_description'
199 UNKNOWN_ERROR_CODE = 'UNKNOWN'
200 UNHANDLED_ERROR_CODE = 'UNHANDLED'
201
202 MAX_KEY_LENGTH = 0xff
203 MAX_VALUE_LENGTH = 0xffff
204
205
206 class IArgumentType(Interface):
207     """
208     An L{IArgumentType} can serialize a Python object into an AMP box and
209     deserialize information from an AMP box back into a Python object.
210
211     @since: 9.0
212     """
213     def fromBox(name, strings, objects, proto):
214         """
215         Given an argument name and an AMP box containing serialized values,
216         extract one or more Python objects and add them to the C{objects}
217         dictionary.
218
219         @param name: The name associated with this argument.  Most commonly,
220             this is the key which can be used to find a serialized value in
221             C{strings} and which should be used as the key in C{objects} to
222             associate with a structured Python object.
223         @type name: C{str}
224
225         @param strings: The AMP box from which to extract one or more
226             values.
227         @type strings: C{dict}
228
229         @param objects: The output dictionary to populate with the value for
230             this argument.
231         @type objects: C{dict}
232
233         @param proto: The protocol instance which received the AMP box being
234             interpreted.  Most likely this is an instance of L{AMP}, but
235             this is not guaranteed.
236
237         @return: C{None}
238         """
239
240
241     def toBox(name, strings, objects, proto):
242         """
243         Given an argument name and a dictionary containing structured Python
244         objects, serialize values into one or more strings and add them to
245         the C{strings} dictionary.
246
247         @param name: The name associated with this argument.  Most commonly,
248             this is the key which can be used to find an object in
249             C{objects} and which should be used as the key in C{strings} to
250             associate with a C{str} giving the serialized form of that
251             object.
252         @type name: C{str}
253
254         @param strings: The AMP box into which to insert one or more
255             strings.
256         @type strings: C{dict}
257
258         @param objects: The input dictionary from which to extract Python
259             objects to serialize.
260         @type objects: C{dict}
261
262         @param proto: The protocol instance which will send the AMP box once
263             it is fully populated.  Most likely this is an instance of
264             L{AMP}, but this is not guaranteed.
265
266         @return: C{None}
267         """
268
269
270
271 class IBoxSender(Interface):
272     """
273     A transport which can send L{AmpBox} objects.
274     """
275
276     def sendBox(box):
277         """
278         Send an L{AmpBox}.
279
280         @raise ProtocolSwitched: if the underlying protocol has been
281         switched.
282
283         @raise ConnectionLost: if the underlying connection has already been
284         lost.
285         """
286
287     def unhandledError(failure):
288         """
289         An unhandled error occurred in response to a box.  Log it
290         appropriately.
291
292         @param failure: a L{Failure} describing the error that occurred.
293         """
294
295
296
297 class IBoxReceiver(Interface):
298     """
299     An application object which can receive L{AmpBox} objects and dispatch them
300     appropriately.
301     """
302
303     def startReceivingBoxes(boxSender):
304         """
305         The L{ampBoxReceived} method will start being called; boxes may be
306         responded to by responding to the given L{IBoxSender}.
307
308         @param boxSender: an L{IBoxSender} provider.
309         """
310
311
312     def ampBoxReceived(box):
313         """
314         A box was received from the transport; dispatch it appropriately.
315         """
316
317
318     def stopReceivingBoxes(reason):
319         """
320         No further boxes will be received on this connection.
321
322         @type reason: L{Failure}
323         """
324
325
326
327 class IResponderLocator(Interface):
328     """
329     An application object which can look up appropriate responder methods for
330     AMP commands.
331     """
332
333     def locateResponder(self, name):
334         """
335         Locate a responder method appropriate for the named command.
336
337         @param name: the wire-level name (commandName) of the AMP command to be
338         responded to.
339
340         @return: a 1-argument callable that takes an L{AmpBox} with argument
341         values for the given command, and returns an L{AmpBox} containing
342         argument values for the named command, or a L{Deferred} that fires the
343         same.
344         """
345
346
347
348 class AmpError(Exception):
349     """
350     Base class of all Amp-related exceptions.
351     """
352
353
354
355 class ProtocolSwitched(Exception):
356     """
357     Connections which have been switched to other protocols can no longer
358     accept traffic at the AMP level.  This is raised when you try to send it.
359     """
360
361
362
363 class OnlyOneTLS(AmpError):
364     """
365     This is an implementation limitation; TLS may only be started once per
366     connection.
367     """
368
369
370
371 class NoEmptyBoxes(AmpError):
372     """
373     You can't have empty boxes on the connection.  This is raised when you
374     receive or attempt to send one.
375     """
376
377
378
379 class InvalidSignature(AmpError):
380     """
381     You didn't pass all the required arguments.
382     """
383
384
385
386 class TooLong(AmpError):
387     """
388     One of the protocol's length limitations was violated.
389
390     @ivar isKey: true if the string being encoded in a key position, false if
391     it was in a value position.
392
393     @ivar isLocal: Was the string encoded locally, or received too long from
394     the network?  (It's only physically possible to encode "too long" values on
395     the network for keys.)
396
397     @ivar value: The string that was too long.
398
399     @ivar keyName: If the string being encoded was in a value position, what
400     key was it being encoded for?
401     """
402
403     def __init__(self, isKey, isLocal, value, keyName=None):
404         AmpError.__init__(self)
405         self.isKey = isKey
406         self.isLocal = isLocal
407         self.value = value
408         self.keyName = keyName
409
410
411     def __repr__(self):
412         hdr = self.isKey and "key" or "value"
413         if not self.isKey:
414             hdr += ' ' + repr(self.keyName)
415         lcl = self.isLocal and "local" or "remote"
416         return "%s %s too long: %d" % (lcl, hdr, len(self.value))
417
418
419
420 class BadLocalReturn(AmpError):
421     """
422     A bad value was returned from a local command; we were unable to coerce it.
423     """
424     def __init__(self, message, enclosed):
425         AmpError.__init__(self)
426         self.message = message
427         self.enclosed = enclosed
428
429
430     def __repr__(self):
431         return self.message + " " + self.enclosed.getBriefTraceback()
432
433     __str__ = __repr__
434
435
436
437 class RemoteAmpError(AmpError):
438     """
439     This error indicates that something went wrong on the remote end of the
440     connection, and the error was serialized and transmitted to you.
441     """
442     def __init__(self, errorCode, description, fatal=False, local=None):
443         """Create a remote error with an error code and description.
444
445         @param errorCode: the AMP error code of this error.
446
447         @param description: some text to show to the user.
448
449         @param fatal: a boolean, true if this error should terminate the
450         connection.
451
452         @param local: a local Failure, if one exists.
453         """
454         if local:
455             localwhat = ' (local)'
456             othertb = local.getBriefTraceback()
457         else:
458             localwhat = ''
459             othertb = ''
460         Exception.__init__(self, "Code<%s>%s: %s%s" % (
461                 errorCode, localwhat,
462                 description, othertb))
463         self.local = local
464         self.errorCode = errorCode
465         self.description = description
466         self.fatal = fatal
467
468
469
470 class UnknownRemoteError(RemoteAmpError):
471     """
472     This means that an error whose type we can't identify was raised from the
473     other side.
474     """
475     def __init__(self, description):
476         errorCode = UNKNOWN_ERROR_CODE
477         RemoteAmpError.__init__(self, errorCode, description)
478
479
480
481 class MalformedAmpBox(AmpError):
482     """
483     This error indicates that the wire-level protocol was malformed.
484     """
485
486
487
488 class UnhandledCommand(AmpError):
489     """
490     A command received via amp could not be dispatched.
491     """
492
493
494
495 class IncompatibleVersions(AmpError):
496     """
497     It was impossible to negotiate a compatible version of the protocol with
498     the other end of the connection.
499     """
500
501
502 PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand}
503
504 class AmpBox(dict):
505     """
506     I am a packet in the AMP protocol, much like a regular str:str dictionary.
507     """
508     __slots__ = []              # be like a regular dictionary, don't magically
509                                 # acquire a __dict__...
510
511
512     def copy(self):
513         """
514         Return another AmpBox just like me.
515         """
516         newBox = self.__class__()
517         newBox.update(self)
518         return newBox
519
520
521     def serialize(self):
522         """
523         Convert me into a wire-encoded string.
524
525         @return: a str encoded according to the rules described in the module
526         docstring.
527         """
528         i = self.items()
529         i.sort()
530         L = []
531         w = L.append
532         for k, v in i:
533             if len(k) > MAX_KEY_LENGTH:
534                 raise TooLong(True, True, k, None)
535             if len(v) > MAX_VALUE_LENGTH:
536                 raise TooLong(False, True, v, k)
537             for kv in k, v:
538                 w(pack("!H", len(kv)))
539                 w(kv)
540         w(pack("!H", 0))
541         return ''.join(L)
542
543
544     def _sendTo(self, proto):
545         """
546         Serialize and send this box to a Amp instance.  By the time it is being
547         sent, several keys are required.  I must have exactly ONE of::
548
549             _ask
550             _answer
551             _error
552
553         If the '_ask' key is set, then the '_command' key must also be
554         set.
555
556         @param proto: an AMP instance.
557         """
558         proto.sendBox(self)
559
560     def __repr__(self):
561         return 'AmpBox(%s)' % (dict.__repr__(self),)
562
563 # amp.Box => AmpBox
564
565 Box = AmpBox
566
567 class QuitBox(AmpBox):
568     """
569     I am an AmpBox that, upon being sent, terminates the connection.
570     """
571     __slots__ = []
572
573
574     def __repr__(self):
575         return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),)
576
577
578     def _sendTo(self, proto):
579         """
580         Immediately call loseConnection after sending.
581         """
582         super(QuitBox, self)._sendTo(proto)
583         proto.transport.loseConnection()
584
585
586
587 class _SwitchBox(AmpBox):
588     """
589     Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets
590     up state for the protocol to switch.
591     """
592
593     # DON'T set __slots__ here; we do have an attribute.
594
595     def __init__(self, innerProto, **kw):
596         """
597         Create a _SwitchBox with the protocol to switch to after being sent.
598
599         @param innerProto: the protocol instance to switch to.
600         @type innerProto: an IProtocol provider.
601         """
602         super(_SwitchBox, self).__init__(**kw)
603         self.innerProto = innerProto
604
605
606     def __repr__(self):
607         return '_SwitchBox(%r, **%s)' % (self.innerProto,
608                                          dict.__repr__(self),)
609
610
611     def _sendTo(self, proto):
612         """
613         Send me; I am the last box on the connection.  All further traffic will be
614         over the new protocol.
615         """
616         super(_SwitchBox, self)._sendTo(proto)
617         proto._lockForSwitch()
618         proto._switchTo(self.innerProto)
619
620
621
622 class BoxDispatcher:
623     """
624     A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es,
625     both incoming and outgoing, to their appropriate destinations.
626
627     Outgoing commands are converted into L{Deferred}s and outgoing boxes, and
628     associated tracking state to fire those L{Deferred} when '_answer' boxes
629     come back.  Incoming '_answer' and '_error' boxes are converted into
630     callbacks and errbacks on those L{Deferred}s, respectively.
631
632     Incoming '_ask' boxes are converted into method calls on a supplied method
633     locator.
634
635     @ivar _outstandingRequests: a dictionary mapping request IDs to
636     L{Deferred}s which were returned for those requests.
637
638     @ivar locator: an object with a L{locateResponder} method that locates a
639     responder function that takes a Box and returns a result (either a Box or a
640     Deferred which fires one).
641
642     @ivar boxSender: an object which can send boxes, via the L{_sendBox}
643     method, such as an L{AMP} instance.
644     @type boxSender: L{IBoxSender}
645     """
646
647     implements(IBoxReceiver)
648
649     _failAllReason = None
650     _outstandingRequests = None
651     _counter = 0L
652     boxSender = None
653
654     def __init__(self, locator):
655         self._outstandingRequests = {}
656         self.locator = locator
657
658
659     def startReceivingBoxes(self, boxSender):
660         """
661         The given boxSender is going to start calling boxReceived on this
662         L{BoxDispatcher}.
663
664         @param boxSender: The L{IBoxSender} to send command responses to.
665         """
666         self.boxSender = boxSender
667
668
669     def stopReceivingBoxes(self, reason):
670         """
671         No further boxes will be received here.  Terminate all currently
672         oustanding command deferreds with the given reason.
673         """
674         self.failAllOutgoing(reason)
675
676
677     def failAllOutgoing(self, reason):
678         """
679         Call the errback on all outstanding requests awaiting responses.
680
681         @param reason: the Failure instance to pass to those errbacks.
682         """
683         self._failAllReason = reason
684         OR = self._outstandingRequests.items()
685         self._outstandingRequests = None # we can never send another request
686         for key, value in OR:
687             value.errback(reason)
688
689
690     def _nextTag(self):
691         """
692         Generate protocol-local serial numbers for _ask keys.
693
694         @return: a string that has not yet been used on this connection.
695         """
696         self._counter += 1
697         return '%x' % (self._counter,)
698
699
700     def _sendBoxCommand(self, command, box, requiresAnswer=True):
701         """
702         Send a command across the wire with the given C{amp.Box}.
703
704         Mutate the given box to give it any additional keys (_command, _ask)
705         required for the command and request/response machinery, then send it.
706
707         If requiresAnswer is True, returns a C{Deferred} which fires when a
708         response is received. The C{Deferred} is fired with an C{amp.Box} on
709         success, or with an C{amp.RemoteAmpError} if an error is received.
710
711         If the Deferred fails and the error is not handled by the caller of
712         this method, the failure will be logged and the connection dropped.
713
714         @param command: a str, the name of the command to issue.
715
716         @param box: an AmpBox with the arguments for the command.
717
718         @param requiresAnswer: a boolean.  Defaults to True.  If True, return a
719         Deferred which will fire when the other side responds to this command.
720         If False, return None and do not ask the other side for acknowledgement.
721
722         @return: a Deferred which fires the AmpBox that holds the response to
723         this command, or None, as specified by requiresAnswer.
724
725         @raise ProtocolSwitched: if the protocol has been switched.
726         """
727         if self._failAllReason is not None:
728             return fail(self._failAllReason)
729         box[COMMAND] = command
730         tag = self._nextTag()
731         if requiresAnswer:
732             box[ASK] = tag
733         box._sendTo(self.boxSender)
734         if requiresAnswer:
735             result = self._outstandingRequests[tag] = Deferred()
736         else:
737             result = None
738         return result
739
740
741     def callRemoteString(self, command, requiresAnswer=True, **kw):
742         """
743         This is a low-level API, designed only for optimizing simple messages
744         for which the overhead of parsing is too great.
745
746         @param command: a str naming the command.
747
748         @param kw: arguments to the amp box.
749
750         @param requiresAnswer: a boolean.  Defaults to True.  If True, return a
751         Deferred which will fire when the other side responds to this command.
752         If False, return None and do not ask the other side for acknowledgement.
753
754         @return: a Deferred which fires the AmpBox that holds the response to
755         this command, or None, as specified by requiresAnswer.
756         """
757         box = Box(kw)
758         return self._sendBoxCommand(command, box)
759
760
761     def callRemote(self, commandType, *a, **kw):
762         """
763         This is the primary high-level API for sending messages via AMP.  Invoke it
764         with a command and appropriate arguments to send a message to this
765         connection's peer.
766
767         @param commandType: a subclass of Command.
768         @type commandType: L{type}
769
770         @param a: Positional (special) parameters taken by the command.
771         Positional parameters will typically not be sent over the wire.  The
772         only command included with AMP which uses positional parameters is
773         L{ProtocolSwitchCommand}, which takes the protocol that will be
774         switched to as its first argument.
775
776         @param kw: Keyword arguments taken by the command.  These are the
777         arguments declared in the command's 'arguments' attribute.  They will
778         be encoded and sent to the peer as arguments for the L{commandType}.
779
780         @return: If L{commandType} has a C{requiresAnswer} attribute set to
781         L{False}, then return L{None}.  Otherwise, return a L{Deferred} which
782         fires with a dictionary of objects representing the result of this
783         call.  Additionally, this L{Deferred} may fail with an exception
784         representing a connection failure, with L{UnknownRemoteError} if the
785         other end of the connection fails for an unknown reason, or with any
786         error specified as a key in L{commandType}'s C{errors} dictionary.
787         """
788
789         # XXX this takes command subclasses and not command objects on purpose.
790         # There's really no reason to have all this back-and-forth between
791         # command objects and the protocol, and the extra object being created
792         # (the Command instance) is pointless.  Command is kind of like
793         # Interface, and should be more like it.
794
795         # In other words, the fact that commandType is instantiated here is an
796         # implementation detail.  Don't rely on it.
797
798         try:
799             co = commandType(*a, **kw)
800         except:
801             return fail()
802         return co._doCommand(self)
803
804
805     def unhandledError(self, failure):
806         """
807         This is a terminal callback called after application code has had a
808         chance to quash any errors.
809         """
810         return self.boxSender.unhandledError(failure)
811
812
813     def _answerReceived(self, box):
814         """
815         An AMP box was received that answered a command previously sent with
816         L{callRemote}.
817
818         @param box: an AmpBox with a value for its L{ANSWER} key.
819         """
820         question = self._outstandingRequests.pop(box[ANSWER])
821         question.addErrback(self.unhandledError)
822         question.callback(box)
823
824
825     def _errorReceived(self, box):
826         """
827         An AMP box was received that answered a command previously sent with
828         L{callRemote}, with an error.
829
830         @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE},
831         and L{ERROR_DESCRIPTION} keys.
832         """
833         question = self._outstandingRequests.pop(box[ERROR])
834         question.addErrback(self.unhandledError)
835         errorCode = box[ERROR_CODE]
836         description = box[ERROR_DESCRIPTION]
837         if errorCode in PROTOCOL_ERRORS:
838             exc = PROTOCOL_ERRORS[errorCode](errorCode, description)
839         else:
840             exc = RemoteAmpError(errorCode, description)
841         question.errback(Failure(exc))
842
843
844     def _commandReceived(self, box):
845         """
846         @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK}
847         keys.
848         """
849         cmd = box[COMMAND]
850         def formatAnswer(answerBox):
851             answerBox[ANSWER] = box[ASK]
852             return answerBox
853         def formatError(error):
854             if error.check(RemoteAmpError):
855                 code = error.value.errorCode
856                 desc = error.value.description
857                 if error.value.fatal:
858                     errorBox = QuitBox()
859                 else:
860                     errorBox = AmpBox()
861             else:
862                 errorBox = QuitBox()
863                 log.err(error) # here is where server-side logging happens
864                                # if the error isn't handled
865                 code = UNKNOWN_ERROR_CODE
866                 desc = "Unknown Error"
867             errorBox[ERROR] = box[ASK]
868             errorBox[ERROR_DESCRIPTION] = desc
869             errorBox[ERROR_CODE] = code
870             return errorBox
871         deferred = self.dispatchCommand(box)
872         if ASK in box:
873             deferred.addCallbacks(formatAnswer, formatError)
874             deferred.addCallback(self._safeEmit)
875         deferred.addErrback(self.unhandledError)
876
877
878     def ampBoxReceived(self, box):
879         """
880         An AmpBox was received, representing a command, or an answer to a
881         previously issued command (either successful or erroneous).  Respond to
882         it according to its contents.
883
884         @param box: an AmpBox
885
886         @raise NoEmptyBoxes: when a box is received that does not contain an
887         '_answer', '_command' / '_ask', or '_error' key; i.e. one which does not
888         fit into the command / response protocol defined by AMP.
889         """
890         if ANSWER in box:
891             self._answerReceived(box)
892         elif ERROR in box:
893             self._errorReceived(box)
894         elif COMMAND in box:
895             self._commandReceived(box)
896         else:
897             raise NoEmptyBoxes(box)
898
899
900     def _safeEmit(self, aBox):
901         """
902         Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors
903         which cannot be usefully handled.
904         """
905         try:
906             aBox._sendTo(self.boxSender)
907         except (ProtocolSwitched, ConnectionLost):
908             pass
909
910
911     def dispatchCommand(self, box):
912         """
913         A box with a _command key was received.
914
915         Dispatch it to a local handler call it.
916
917         @param proto: an AMP instance.
918         @param box: an AmpBox to be dispatched.
919         """
920         cmd = box[COMMAND]
921         responder = self.locator.locateResponder(cmd)
922         if responder is None:
923             return fail(RemoteAmpError(
924                     UNHANDLED_ERROR_CODE,
925                     "Unhandled Command: %r" % (cmd,),
926                     False,
927                     local=Failure(UnhandledCommand())))
928         return maybeDeferred(responder, box)
929
930
931
932 class CommandLocator:
933     """
934     A L{CommandLocator} is a collection of responders to AMP L{Command}s, with
935     the help of the L{Command.responder} decorator.
936     """
937
938     class __metaclass__(type):
939         """
940         This metaclass keeps track of all of the Command.responder-decorated
941         methods defined since the last CommandLocator subclass was defined.  It
942         assumes (usually correctly, but unfortunately not necessarily so) that
943         those commands responders were all declared as methods of the class
944         being defined.  Note that this list can be incorrect if users use the
945         Command.responder decorator outside the context of a CommandLocator
946         class declaration.
947
948         The Command.responder decorator explicitly cooperates with this
949         metaclass.
950         """
951
952         _currentClassCommands = []
953         def __new__(cls, name, bases, attrs):
954             commands = cls._currentClassCommands[:]
955             cls._currentClassCommands[:] = []
956             cd = attrs['_commandDispatch'] = {}
957             for base in bases:
958                 cls._grabFromBase(cd, base)
959             for commandClass, responderFunc in commands:
960                 cd[commandClass.commandName] = (commandClass, responderFunc)
961             subcls = type.__new__(cls, name, bases, attrs)
962             if (bases and (
963                     subcls.lookupFunction != CommandLocator.lookupFunction)):
964                 def locateResponder(self, name):
965                     warnings.warn(
966                         "Override locateResponder, not lookupFunction.",
967                         category=PendingDeprecationWarning,
968                         stacklevel=2)
969                     return self.lookupFunction(name)
970                 subcls.locateResponder = locateResponder
971             return subcls
972
973         def _grabFromBase(cls, cd, base):
974             if hasattr(base, "_commandDispatch"):
975                 cd.update(base._commandDispatch)
976                 for subbase in base.__bases__:
977                     cls._grabFromBase(cd, subbase)
978         _grabFromBase = classmethod(_grabFromBase)
979
980     implements(IResponderLocator)
981
982
983     def _wrapWithSerialization(self, aCallable, command):
984         """
985         Wrap aCallable with its command's argument de-serialization
986         and result serialization logic.
987
988         @param aCallable: a callable with a 'command' attribute, designed to be
989         called with keyword arguments.
990
991         @param command: the command class whose serialization to use.
992
993         @return: a 1-arg callable which, when invoked with an AmpBox, will
994         deserialize the argument list and invoke appropriate user code for the
995         callable's command, returning a Deferred which fires with the result or
996         fails with an error.
997         """
998         def doit(box):
999             kw = command.parseArguments(box, self)
1000             def checkKnownErrors(error):
1001                 key = error.trap(*command.allErrors)
1002                 code = command.allErrors[key]
1003                 desc = str(error.value)
1004                 return Failure(RemoteAmpError(
1005                         code, desc, key in command.fatalErrors, local=error))
1006             def makeResponseFor(objects):
1007                 try:
1008                     return command.makeResponse(objects, self)
1009                 except:
1010                     # let's helpfully log this.
1011                     originalFailure = Failure()
1012                     raise BadLocalReturn(
1013                         "%r returned %r and %r could not serialize it" % (
1014                             aCallable,
1015                             objects,
1016                             command),
1017                         originalFailure)
1018             return maybeDeferred(aCallable, **kw).addCallback(
1019                 makeResponseFor).addErrback(
1020                 checkKnownErrors)
1021         return doit
1022
1023
1024     def lookupFunction(self, name):
1025         """
1026         Deprecated synonym for L{locateResponder}
1027         """
1028         if self.__class__.lookupFunction != CommandLocator.lookupFunction:
1029             return CommandLocator.locateResponder(self, name)
1030         else:
1031             warnings.warn("Call locateResponder, not lookupFunction.",
1032                           category=PendingDeprecationWarning,
1033                           stacklevel=2)
1034         return self.locateResponder(name)
1035
1036
1037     def locateResponder(self, name):
1038         """
1039         Locate a callable to invoke when executing the named command.
1040
1041         @param name: the normalized name (from the wire) of the command.
1042
1043         @return: a 1-argument function that takes a Box and returns a box or a
1044         Deferred which fires a Box, for handling the command identified by the
1045         given name, or None, if no appropriate responder can be found.
1046         """
1047         # Try to find a high-level method to invoke, and if we can't find one,
1048         # fall back to a low-level one.
1049         cd = self._commandDispatch
1050         if name in cd:
1051             commandClass, responderFunc = cd[name]
1052             responderMethod = types.MethodType(
1053                 responderFunc, self, self.__class__)
1054             return self._wrapWithSerialization(responderMethod, commandClass)
1055
1056
1057
1058 class SimpleStringLocator(object):
1059     """
1060     Implement the L{locateResponder} method to do simple, string-based
1061     dispatch.
1062     """
1063
1064     implements(IResponderLocator)
1065
1066     baseDispatchPrefix = 'amp_'
1067
1068     def locateResponder(self, name):
1069         """
1070         Locate a callable to invoke when executing the named command.
1071
1072         @return: a function with the name C{"amp_" + name} on L{self}, or None
1073         if no such function exists.  This function will then be called with the
1074         L{AmpBox} itself as an argument.
1075
1076         @param name: the normalized name (from the wire) of the command.
1077         """
1078         fName = self.baseDispatchPrefix + (name.upper())
1079         return getattr(self, fName, None)
1080
1081
1082
1083 PYTHON_KEYWORDS = [
1084     'and', 'del', 'for', 'is', 'raise', 'assert', 'elif', 'from', 'lambda',
1085     'return', 'break', 'else', 'global', 'not', 'try', 'class', 'except',
1086     'if', 'or', 'while', 'continue', 'exec', 'import', 'pass', 'yield',
1087     'def', 'finally', 'in', 'print']
1088
1089
1090
1091 def _wireNameToPythonIdentifier(key):
1092     """
1093     (Private) Normalize an argument name from the wire for use with Python
1094     code.  If the return value is going to be a python keyword it will be
1095     capitalized.  If it contains any dashes they will be replaced with
1096     underscores.
1097
1098     The rationale behind this method is that AMP should be an inherently
1099     multi-language protocol, so message keys may contain all manner of bizarre
1100     bytes.  This is not a complete solution; there are still forms of arguments
1101     that this implementation will be unable to parse.  However, Python
1102     identifiers share a huge raft of properties with identifiers from many
1103     other languages, so this is a 'good enough' effort for now.  We deal
1104     explicitly with dashes because that is the most likely departure: Lisps
1105     commonly use dashes to separate method names, so protocols initially
1106     implemented in a lisp amp dialect may use dashes in argument or command
1107     names.
1108
1109     @param key: a str, looking something like 'foo-bar-baz' or 'from'
1110
1111     @return: a str which is a valid python identifier, looking something like
1112     'foo_bar_baz' or 'From'.
1113     """
1114     lkey = key.replace("-", "_")
1115     if lkey in PYTHON_KEYWORDS:
1116         return lkey.title()
1117     return lkey
1118
1119
1120
1121 class Argument:
1122     """
1123     Base-class of all objects that take values from Amp packets and convert
1124     them into objects for Python functions.
1125
1126     This implementation of L{IArgumentType} provides several higher-level
1127     hooks for subclasses to override.  See L{toString} and L{fromString}
1128     which will be used to define the behavior of L{IArgumentType.toBox} and
1129     L{IArgumentType.fromBox}, respectively.
1130     """
1131     implements(IArgumentType)
1132
1133     optional = False
1134
1135
1136     def __init__(self, optional=False):
1137         """
1138         Create an Argument.
1139
1140         @param optional: a boolean indicating whether this argument can be
1141         omitted in the protocol.
1142         """
1143         self.optional = optional
1144
1145
1146     def retrieve(self, d, name, proto):
1147         """
1148         Retrieve the given key from the given dictionary, removing it if found.
1149
1150         @param d: a dictionary.
1151
1152         @param name: a key in L{d}.
1153
1154         @param proto: an instance of an AMP.
1155
1156         @raise KeyError: if I am not optional and no value was found.
1157
1158         @return: d[name].
1159         """
1160         if self.optional:
1161             value = d.get(name)
1162             if value is not None:
1163                 del d[name]
1164         else:
1165             value = d.pop(name)
1166         return value
1167
1168
1169     def fromBox(self, name, strings, objects, proto):
1170         """
1171         Populate an 'out' dictionary with mapping names to Python values
1172         decoded from an 'in' AmpBox mapping strings to string values.
1173
1174         @param name: the argument name to retrieve
1175         @type name: str
1176
1177         @param strings: The AmpBox to read string(s) from, a mapping of
1178         argument names to string values.
1179         @type strings: AmpBox
1180
1181         @param objects: The dictionary to write object(s) to, a mapping of
1182         names to Python objects.
1183         @type objects: dict
1184
1185         @param proto: an AMP instance.
1186         """
1187         st = self.retrieve(strings, name, proto)
1188         nk = _wireNameToPythonIdentifier(name)
1189         if self.optional and st is None:
1190             objects[nk] = None
1191         else:
1192             objects[nk] = self.fromStringProto(st, proto)
1193
1194
1195     def toBox(self, name, strings, objects, proto):
1196         """
1197         Populate an 'out' AmpBox with strings encoded from an 'in' dictionary
1198         mapping names to Python values.
1199
1200         @param name: the argument name to retrieve
1201         @type name: str
1202
1203         @param strings: The AmpBox to write string(s) to, a mapping of
1204         argument names to string values.
1205         @type strings: AmpBox
1206
1207         @param objects: The dictionary to read object(s) from, a mapping of
1208         names to Python objects.
1209
1210         @type objects: dict
1211
1212         @param proto: the protocol we are converting for.
1213         @type proto: AMP
1214         """
1215         obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto)
1216         if self.optional and obj is None:
1217             # strings[name] = None
1218             pass
1219         else:
1220             strings[name] = self.toStringProto(obj, proto)
1221
1222
1223     def fromStringProto(self, inString, proto):
1224         """
1225         Convert a string to a Python value.
1226
1227         @param inString: the string to convert.
1228
1229         @param proto: the protocol we are converting for.
1230         @type proto: AMP
1231
1232         @return: a Python object.
1233         """
1234         return self.fromString(inString)
1235
1236
1237     def toStringProto(self, inObject, proto):
1238         """
1239         Convert a Python object to a string.
1240
1241         @param inObject: the object to convert.
1242
1243         @param proto: the protocol we are converting for.
1244         @type proto: AMP
1245         """
1246         return self.toString(inObject)
1247
1248
1249     def fromString(self, inString):
1250         """
1251         Convert a string to a Python object.  Subclasses must implement this.
1252
1253         @param inString: the string to convert.
1254         @type inString: str
1255
1256         @return: the decoded value from inString
1257         """
1258
1259
1260     def toString(self, inObject):
1261         """
1262         Convert a Python object into a string for passing over the network.
1263
1264         @param inObject: an object of the type that this Argument is intended
1265         to deal with.
1266
1267         @return: the wire encoding of inObject
1268         @rtype: str
1269         """
1270
1271
1272
1273 class Integer(Argument):
1274     """
1275     Convert to and from 'int'.
1276     """
1277     fromString = int
1278     def toString(self, inObject):
1279         return str(int(inObject))
1280
1281
1282
1283 class String(Argument):
1284     """
1285     Don't do any conversion at all; just pass through 'str'.
1286     """
1287     def toString(self, inObject):
1288         return inObject
1289
1290
1291     def fromString(self, inString):
1292         return inString
1293
1294
1295
1296 class Float(Argument):
1297     """
1298     Encode floating-point values on the wire as their repr.
1299     """
1300     fromString = float
1301     toString = repr
1302
1303
1304
1305 class Boolean(Argument):
1306     """
1307     Encode True or False as "True" or "False" on the wire.
1308     """
1309     def fromString(self, inString):
1310         if inString == 'True':
1311             return True
1312         elif inString == 'False':
1313             return False
1314         else:
1315             raise TypeError("Bad boolean value: %r" % (inString,))
1316
1317
1318     def toString(self, inObject):
1319         if inObject:
1320             return 'True'
1321         else:
1322             return 'False'
1323
1324
1325
1326 class Unicode(String):
1327     """
1328     Encode a unicode string on the wire as UTF-8.
1329     """
1330
1331     def toString(self, inObject):
1332         # assert isinstance(inObject, unicode)
1333         return String.toString(self, inObject.encode('utf-8'))
1334
1335
1336     def fromString(self, inString):
1337         # assert isinstance(inString, str)
1338         return String.fromString(self, inString).decode('utf-8')
1339
1340
1341
1342 class Path(Unicode):
1343     """
1344     Encode and decode L{filepath.FilePath} instances as paths on the wire.
1345
1346     This is really intended for use with subprocess communication tools:
1347     exchanging pathnames on different machines over a network is not generally
1348     meaningful, but neither is it disallowed; you can use this to communicate
1349     about NFS paths, for example.
1350     """
1351     def fromString(self, inString):
1352         return filepath.FilePath(Unicode.fromString(self, inString))
1353
1354
1355     def toString(self, inObject):
1356         return Unicode.toString(self, inObject.path)
1357
1358
1359
1360 class AmpList(Argument):
1361     """
1362     Convert a list of dictionaries into a list of AMP boxes on the wire.
1363
1364     For example, if you want to pass::
1365
1366         [{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}]
1367
1368     You might use an AmpList like this in your arguments or response list::
1369
1370         AmpList([('a', Integer()),
1371                  ('b', Unicode())])
1372     """
1373     def __init__(self, subargs):
1374         """
1375         Create an AmpList.
1376
1377         @param subargs: a list of 2-tuples of ('name', argument) describing the
1378         schema of the dictionaries in the sequence of amp boxes.
1379         """
1380         self.subargs = subargs
1381
1382
1383     def fromStringProto(self, inString, proto):
1384         boxes = parseString(inString)
1385         values = [_stringsToObjects(box, self.subargs, proto)
1386                   for box in boxes]
1387         return values
1388
1389
1390     def toStringProto(self, inObject, proto):
1391         return ''.join([_objectsToStrings(
1392                     objects, self.subargs, Box(), proto
1393                     ).serialize() for objects in inObject])
1394
1395 class Command:
1396     """
1397     Subclass me to specify an AMP Command.
1398
1399     @cvar arguments: A list of 2-tuples of (name, Argument-subclass-instance),
1400     specifying the names and values of the parameters which are required for
1401     this command.
1402
1403     @cvar response: A list like L{arguments}, but instead used for the return
1404     value.
1405
1406     @cvar errors: A mapping of subclasses of L{Exception} to wire-protocol tags
1407     for errors represented as L{str}s.  Responders which raise keys from this
1408     dictionary will have the error translated to the corresponding tag on the
1409     wire.  Invokers which receive Deferreds from invoking this command with
1410     L{AMP.callRemote} will potentially receive Failures with keys from this
1411     mapping as their value.  This mapping is inherited; if you declare a
1412     command which handles C{FooError} as 'FOO_ERROR', then subclass it and
1413     specify C{BarError} as 'BAR_ERROR', responders to the subclass may raise
1414     either C{FooError} or C{BarError}, and invokers must be able to deal with
1415     either of those exceptions.
1416
1417     @cvar fatalErrors: like 'errors', but errors in this list will always
1418     terminate the connection, despite being of a recognizable error type.
1419
1420     @cvar commandType: The type of Box used to issue commands; useful only for
1421     protocol-modifying behavior like startTLS or protocol switching.  Defaults
1422     to a plain vanilla L{Box}.
1423
1424     @cvar responseType: The type of Box used to respond to this command; only
1425     useful for protocol-modifying behavior like startTLS or protocol switching.
1426     Defaults to a plain vanilla L{Box}.
1427
1428     @ivar requiresAnswer: a boolean; defaults to True.  Set it to False on your
1429     subclass if you want callRemote to return None.  Note: this is a hint only
1430     to the client side of the protocol.  The return-type of a command responder
1431     method must always be a dictionary adhering to the contract specified by
1432     L{response}, because clients are always free to request a response if they
1433     want one.
1434     """
1435
1436     class __metaclass__(type):
1437         """
1438         Metaclass hack to establish reverse-mappings for 'errors' and
1439         'fatalErrors' as class vars.
1440         """
1441         def __new__(cls, name, bases, attrs):
1442             re = attrs['reverseErrors'] = {}
1443             er = attrs['allErrors'] = {}
1444             if 'commandName' not in attrs:
1445                 attrs['commandName'] = name
1446             newtype = type.__new__(cls, name, bases, attrs)
1447             errors = {}
1448             fatalErrors = {}
1449             accumulateClassDict(newtype, 'errors', errors)
1450             accumulateClassDict(newtype, 'fatalErrors', fatalErrors)
1451             for v, k in errors.iteritems():
1452                 re[k] = v
1453                 er[v] = k
1454             for v, k in fatalErrors.iteritems():
1455                 re[k] = v
1456                 er[v] = k
1457             return newtype
1458
1459     arguments = []
1460     response = []
1461     extra = []
1462     errors = {}
1463     fatalErrors = {}
1464
1465     commandType = Box
1466     responseType = Box
1467
1468     requiresAnswer = True
1469
1470
1471     def __init__(self, **kw):
1472         """
1473         Create an instance of this command with specified values for its
1474         parameters.
1475
1476         @param kw: a dict containing an appropriate value for each name
1477         specified in the L{arguments} attribute of my class.
1478
1479         @raise InvalidSignature: if you forgot any required arguments.
1480         """
1481         self.structured = kw
1482         givenArgs = kw.keys()
1483         forgotten = []
1484         for name, arg in self.arguments:
1485             pythonName = _wireNameToPythonIdentifier(name)
1486             if pythonName not in givenArgs and not arg.optional:
1487                 forgotten.append(pythonName)
1488         if forgotten:
1489             raise InvalidSignature("forgot %s for %s" % (
1490                     ', '.join(forgotten), self.commandName))
1491         forgotten = []
1492
1493
1494     def makeResponse(cls, objects, proto):
1495         """
1496         Serialize a mapping of arguments using this L{Command}'s
1497         response schema.
1498
1499         @param objects: a dict with keys matching the names specified in
1500         self.response, having values of the types that the Argument objects in
1501         self.response can format.
1502
1503         @param proto: an L{AMP}.
1504
1505         @return: an L{AmpBox}.
1506         """
1507         try:
1508             responseType = cls.responseType()
1509         except:
1510             return fail()
1511         return _objectsToStrings(objects, cls.response, responseType, proto)
1512     makeResponse = classmethod(makeResponse)
1513
1514
1515     def makeArguments(cls, objects, proto):
1516         """
1517         Serialize a mapping of arguments using this L{Command}'s
1518         argument schema.
1519
1520         @param objects: a dict with keys similar to the names specified in
1521         self.arguments, having values of the types that the Argument objects in
1522         self.arguments can parse.
1523
1524         @param proto: an L{AMP}.
1525
1526         @return: An instance of this L{Command}'s C{commandType}.
1527         """
1528         allowedNames = set()
1529         for (argName, ignored) in cls.arguments:
1530             allowedNames.add(_wireNameToPythonIdentifier(argName))
1531
1532         for intendedArg in objects:
1533             if intendedArg not in allowedNames:
1534                 raise InvalidSignature(
1535                     "%s is not a valid argument" % (intendedArg,))
1536         return _objectsToStrings(objects, cls.arguments, cls.commandType(),
1537                                  proto)
1538     makeArguments = classmethod(makeArguments)
1539
1540
1541     def parseResponse(cls, box, protocol):
1542         """
1543         Parse a mapping of serialized arguments using this
1544         L{Command}'s response schema.
1545
1546         @param box: A mapping of response-argument names to the
1547         serialized forms of those arguments.
1548         @param protocol: The L{AMP} protocol.
1549
1550         @return: A mapping of response-argument names to the parsed
1551         forms.
1552         """
1553         return _stringsToObjects(box, cls.response, protocol)
1554     parseResponse = classmethod(parseResponse)
1555
1556
1557     def parseArguments(cls, box, protocol):
1558         """
1559         Parse a mapping of serialized arguments using this
1560         L{Command}'s argument schema.
1561
1562         @param box: A mapping of argument names to the seralized forms
1563         of those arguments.
1564         @param protocol: The L{AMP} protocol.
1565
1566         @return: A mapping of argument names to the parsed forms.
1567         """
1568         return _stringsToObjects(box, cls.arguments, protocol)
1569     parseArguments = classmethod(parseArguments)
1570
1571
1572     def responder(cls, methodfunc):
1573         """
1574         Declare a method to be a responder for a particular command.
1575
1576         This is a decorator.
1577
1578         Use like so::
1579
1580             class MyCommand(Command):
1581                 arguments = [('a', ...), ('b', ...)]
1582
1583             class MyProto(AMP):
1584                 def myFunMethod(self, a, b):
1585                     ...
1586                 MyCommand.responder(myFunMethod)
1587
1588         Notes: Although decorator syntax is not used within Twisted, this
1589         function returns its argument and is therefore safe to use with
1590         decorator syntax.
1591
1592         This is not thread safe.  Don't declare AMP subclasses in other
1593         threads.  Don't declare responders outside the scope of AMP subclasses;
1594         the behavior is undefined.
1595
1596         @param methodfunc: A function which will later become a method, which
1597         has a keyword signature compatible with this command's L{argument} list
1598         and returns a dictionary with a set of keys compatible with this
1599         command's L{response} list.
1600
1601         @return: the methodfunc parameter.
1602         """
1603         CommandLocator._currentClassCommands.append((cls, methodfunc))
1604         return methodfunc
1605     responder = classmethod(responder)
1606
1607
1608     # Our only instance method
1609     def _doCommand(self, proto):
1610         """
1611         Encode and send this Command to the given protocol.
1612
1613         @param proto: an AMP, representing the connection to send to.
1614
1615         @return: a Deferred which will fire or error appropriately when the
1616         other side responds to the command (or error if the connection is lost
1617         before it is responded to).
1618         """
1619
1620         def _massageError(error):
1621             error.trap(RemoteAmpError)
1622             rje = error.value
1623             errorType = self.reverseErrors.get(rje.errorCode,
1624                                                UnknownRemoteError)
1625             return Failure(errorType(rje.description))
1626
1627         d = proto._sendBoxCommand(self.commandName,
1628                                   self.makeArguments(self.structured, proto),
1629                                   self.requiresAnswer)
1630
1631         if self.requiresAnswer:
1632             d.addCallback(self.parseResponse, proto)
1633             d.addErrback(_massageError)
1634
1635         return d
1636
1637
1638
1639 class _NoCertificate:
1640     """
1641     This is for peers which don't want to use a local certificate.  Used by
1642     AMP because AMP's internal language is all about certificates and this
1643     duck-types in the appropriate place; this API isn't really stable though,
1644     so it's not exposed anywhere public.
1645
1646     For clients, it will use ephemeral DH keys, or whatever the default is for
1647     certificate-less clients in OpenSSL.  For servers, it will generate a
1648     temporary self-signed certificate with garbage values in the DN and use
1649     that.
1650     """
1651
1652     def __init__(self, client):
1653         """
1654         Create a _NoCertificate which either is or isn't for the client side of
1655         the connection.
1656
1657         @param client: True if we are a client and should truly have no
1658         certificate and be anonymous, False if we are a server and actually
1659         have to generate a temporary certificate.
1660
1661         @type client: bool
1662         """
1663         self.client = client
1664
1665
1666     def options(self, *authorities):
1667         """
1668         Behaves like L{twisted.internet.ssl.PrivateCertificate.options}().
1669         """
1670         if not self.client:
1671             # do some crud with sslverify to generate a temporary self-signed
1672             # certificate.  This is SLOOOWWWWW so it is only in the absolute
1673             # worst, most naive case.
1674
1675             # We have to do this because OpenSSL will not let both the server
1676             # and client be anonymous.
1677             sharedDN = DN(CN='TEMPORARY CERTIFICATE')
1678             key = KeyPair.generate()
1679             cr = key.certificateRequest(sharedDN)
1680             sscrd = key.signCertificateRequest(sharedDN, cr, lambda dn: True, 1)
1681             cert = key.newCertificate(sscrd)
1682             return cert.options(*authorities)
1683         options = dict()
1684         if authorities:
1685             options.update(dict(verify=True,
1686                                 requireCertificate=True,
1687                                 caCerts=[auth.original for auth in authorities]))
1688         occo = CertificateOptions(**options)
1689         return occo
1690
1691
1692
1693 class _TLSBox(AmpBox):
1694     """
1695     I am an AmpBox that, upon being sent, initiates a TLS connection.
1696     """
1697     __slots__ = []
1698
1699     def __init__(self):
1700         if ssl is None:
1701             raise RemoteAmpError("TLS_ERROR", "TLS not available")
1702         AmpBox.__init__(self)
1703
1704
1705     def _keyprop(k, default):
1706         return property(lambda self: self.get(k, default))
1707
1708
1709     # These properties are described in startTLS
1710     certificate = _keyprop('tls_localCertificate', _NoCertificate(False))
1711     verify = _keyprop('tls_verifyAuthorities', None)
1712
1713     def _sendTo(self, proto):
1714         """
1715         Send my encoded value to the protocol, then initiate TLS.
1716         """
1717         ab = AmpBox(self)
1718         for k in ['tls_localCertificate',
1719                   'tls_verifyAuthorities']:
1720             ab.pop(k, None)
1721         ab._sendTo(proto)
1722         proto._startTLS(self.certificate, self.verify)
1723
1724
1725
1726 class _LocalArgument(String):
1727     """
1728     Local arguments are never actually relayed across the wire.  This is just a
1729     shim so that StartTLS can pretend to have some arguments: if arguments
1730     acquire documentation properties, replace this with something nicer later.
1731     """
1732
1733     def fromBox(self, name, strings, objects, proto):
1734         pass
1735
1736
1737
1738 class StartTLS(Command):
1739     """
1740     Use, or subclass, me to implement a command that starts TLS.
1741
1742     Callers of StartTLS may pass several special arguments, which affect the
1743     TLS negotiation:
1744
1745         - tls_localCertificate: This is a
1746         twisted.internet.ssl.PrivateCertificate which will be used to secure
1747         the side of the connection it is returned on.
1748
1749         - tls_verifyAuthorities: This is a list of
1750         twisted.internet.ssl.Certificate objects that will be used as the
1751         certificate authorities to verify our peer's certificate.
1752
1753     Each of those special parameters may also be present as a key in the
1754     response dictionary.
1755     """
1756
1757     arguments = [("tls_localCertificate", _LocalArgument(optional=True)),
1758                  ("tls_verifyAuthorities", _LocalArgument(optional=True))]
1759
1760     response = [("tls_localCertificate", _LocalArgument(optional=True)),
1761                 ("tls_verifyAuthorities", _LocalArgument(optional=True))]
1762
1763     responseType = _TLSBox
1764
1765     def __init__(self, **kw):
1766         """
1767         Create a StartTLS command.  (This is private.  Use AMP.callRemote.)
1768
1769         @param tls_localCertificate: the PrivateCertificate object to use to
1770         secure the connection.  If it's None, or unspecified, an ephemeral DH
1771         key is used instead.
1772
1773         @param tls_verifyAuthorities: a list of Certificate objects which
1774         represent root certificates to verify our peer with.
1775         """
1776         if ssl is None:
1777             raise RuntimeError("TLS not available.")
1778         self.certificate = kw.pop('tls_localCertificate', _NoCertificate(True))
1779         self.authorities = kw.pop('tls_verifyAuthorities', None)
1780         Command.__init__(self, **kw)
1781
1782
1783     def _doCommand(self, proto):
1784         """
1785         When a StartTLS command is sent, prepare to start TLS, but don't actually
1786         do it; wait for the acknowledgement, then initiate the TLS handshake.
1787         """
1788         d = Command._doCommand(self, proto)
1789         proto._prepareTLS(self.certificate, self.authorities)
1790         # XXX before we get back to user code we are going to start TLS...
1791         def actuallystart(response):
1792             proto._startTLS(self.certificate, self.authorities)
1793             return response
1794         d.addCallback(actuallystart)
1795         return d
1796
1797
1798
1799 class ProtocolSwitchCommand(Command):
1800     """
1801     Use this command to switch from something Amp-derived to a different
1802     protocol mid-connection.  This can be useful to use amp as the
1803     connection-startup negotiation phase.  Since TLS is a different layer
1804     entirely, you can use Amp to negotiate the security parameters of your
1805     connection, then switch to a different protocol, and the connection will
1806     remain secured.
1807     """
1808
1809     def __init__(self, _protoToSwitchToFactory, **kw):
1810         """
1811         Create a ProtocolSwitchCommand.
1812
1813         @param _protoToSwitchToFactory: a ProtocolFactory which will generate
1814         the Protocol to switch to.
1815
1816         @param kw: Keyword arguments, encoded and handled normally as
1817         L{Command} would.
1818         """
1819
1820         self.protoToSwitchToFactory = _protoToSwitchToFactory
1821         super(ProtocolSwitchCommand, self).__init__(**kw)
1822
1823
1824     def makeResponse(cls, innerProto, proto):
1825         return _SwitchBox(innerProto)
1826     makeResponse = classmethod(makeResponse)
1827
1828
1829     def _doCommand(self, proto):
1830         """
1831         When we emit a ProtocolSwitchCommand, lock the protocol, but don't actually
1832         switch to the new protocol unless an acknowledgement is received.  If
1833         an error is received, switch back.
1834         """
1835         d = super(ProtocolSwitchCommand, self)._doCommand(proto)
1836         proto._lockForSwitch()
1837         def switchNow(ign):
1838             innerProto = self.protoToSwitchToFactory.buildProtocol(
1839                 proto.transport.getPeer())
1840             proto._switchTo(innerProto, self.protoToSwitchToFactory)
1841             return ign
1842         def handle(ign):
1843             proto._unlockFromSwitch()
1844             self.protoToSwitchToFactory.clientConnectionFailed(
1845                 None, Failure(CONNECTION_LOST))
1846             return ign
1847         return d.addCallbacks(switchNow, handle)
1848
1849
1850
1851 class BinaryBoxProtocol(StatefulStringProtocol, Int16StringReceiver):
1852     """
1853     A protocol for receving L{Box}es - key/value pairs - via length-prefixed
1854     strings.  A box is composed of:
1855
1856         - any number of key-value pairs, described by:
1857             - a 2-byte network-endian packed key length (of which the first
1858               byte must be null, and the second must be non-null: i.e. the
1859               value of the length must be 1-255)
1860             - a key, comprised of that many bytes
1861             - a 2-byte network-endian unsigned value length (up to the maximum
1862               of 65535)
1863             - a value, comprised of that many bytes
1864         - 2 null bytes
1865
1866     In other words, an even number of strings prefixed with packed unsigned
1867     16-bit integers, and then a 0-length string to indicate the end of the box.
1868
1869     This protocol also implements 2 extra private bits of functionality related
1870     to the byte boundaries between messages; it can start TLS between two given
1871     boxes or switch to an entirely different protocol.  However, due to some
1872     tricky elements of the implementation, the public interface to this
1873     functionality is L{ProtocolSwitchCommand} and L{StartTLS}.
1874
1875     @ivar _keyLengthLimitExceeded: A flag which is only true when the
1876         connection is being closed because a key length prefix which was longer
1877         than allowed by the protocol was received.
1878
1879     @ivar boxReceiver: an L{IBoxReceiver} provider, whose L{ampBoxReceived}
1880     method will be invoked for each L{Box} that is received.
1881     """
1882
1883     implements(IBoxSender)
1884
1885     _justStartedTLS = False
1886     _startingTLSBuffer = None
1887     _locked = False
1888     _currentKey = None
1889     _currentBox = None
1890
1891     _keyLengthLimitExceeded = False
1892
1893     hostCertificate = None
1894     noPeerCertificate = False   # for tests
1895     innerProtocol = None
1896     innerProtocolClientFactory = None
1897
1898     def __init__(self, boxReceiver):
1899         self.boxReceiver = boxReceiver
1900
1901
1902     def _switchTo(self, newProto, clientFactory=None):
1903         """
1904         Switch this BinaryBoxProtocol's transport to a new protocol.  You need
1905         to do this 'simultaneously' on both ends of a connection; the easiest
1906         way to do this is to use a subclass of ProtocolSwitchCommand.
1907
1908         @param newProto: the new protocol instance to switch to.
1909
1910         @param clientFactory: the ClientFactory to send the
1911         L{clientConnectionLost} notification to.
1912         """
1913         # All the data that Int16Receiver has not yet dealt with belongs to our
1914         # new protocol: luckily it's keeping that in a handy (although
1915         # ostensibly internal) variable for us:
1916         newProtoData = self.recvd
1917         # We're quite possibly in the middle of a 'dataReceived' loop in
1918         # Int16StringReceiver: let's make sure that the next iteration, the
1919         # loop will break and not attempt to look at something that isn't a
1920         # length prefix.
1921         self.recvd = ''
1922         # Finally, do the actual work of setting up the protocol and delivering
1923         # its first chunk of data, if one is available.
1924         self.innerProtocol = newProto
1925         self.innerProtocolClientFactory = clientFactory
1926         newProto.makeConnection(self.transport)
1927         newProto.dataReceived(newProtoData)
1928
1929
1930     def sendBox(self, box):
1931         """
1932         Send a amp.Box to my peer.
1933
1934         Note: transport.write is never called outside of this method.
1935
1936         @param box: an AmpBox.
1937
1938         @raise ProtocolSwitched: if the protocol has previously been switched.
1939
1940         @raise ConnectionLost: if the connection has previously been lost.
1941         """
1942         if self._locked:
1943             raise ProtocolSwitched(
1944                 "This connection has switched: no AMP traffic allowed.")
1945         if self.transport is None:
1946             raise ConnectionLost()
1947         if self._startingTLSBuffer is not None:
1948             self._startingTLSBuffer.append(box)
1949         else:
1950             self.transport.write(box.serialize())
1951
1952
1953     def makeConnection(self, transport):
1954         """
1955         Notify L{boxReceiver} that it is about to receive boxes from this
1956         protocol by invoking L{startReceivingBoxes}.
1957         """
1958         self.transport = transport
1959         self.boxReceiver.startReceivingBoxes(self)
1960         self.connectionMade()
1961
1962
1963     def dataReceived(self, data):
1964         """
1965         Either parse incoming data as L{AmpBox}es or relay it to our nested
1966         protocol.
1967         """
1968         if self._justStartedTLS:
1969             self._justStartedTLS = False
1970         # If we already have an inner protocol, then we don't deliver data to
1971         # the protocol parser any more; we just hand it off.
1972         if self.innerProtocol is not None:
1973             self.innerProtocol.dataReceived(data)
1974             return
1975         return Int16StringReceiver.dataReceived(self, data)
1976
1977
1978     def connectionLost(self, reason):
1979         """
1980         The connection was lost; notify any nested protocol.
1981         """
1982         if self.innerProtocol is not None:
1983             self.innerProtocol.connectionLost(reason)
1984             if self.innerProtocolClientFactory is not None:
1985                 self.innerProtocolClientFactory.clientConnectionLost(None, reason)
1986         if self._keyLengthLimitExceeded:
1987             failReason = Failure(TooLong(True, False, None, None))
1988         elif reason.check(ConnectionClosed) and self._justStartedTLS:
1989             # We just started TLS and haven't received any data.  This means
1990             # the other connection didn't like our cert (although they may not
1991             # have told us why - later Twisted should make 'reason' into a TLS
1992             # error.)
1993             failReason = PeerVerifyError(
1994                 "Peer rejected our certificate for an unknown reason.")
1995         else:
1996             failReason = reason
1997         self.boxReceiver.stopReceivingBoxes(failReason)
1998
1999
2000     # The longest key allowed
2001     _MAX_KEY_LENGTH = 255
2002
2003     # The longest value allowed (this is somewhat redundant, as longer values
2004     # cannot be encoded - ah well).
2005     _MAX_VALUE_LENGTH = 65535
2006
2007     # The first thing received is a key.
2008     MAX_LENGTH = _MAX_KEY_LENGTH
2009
2010     def proto_init(self, string):
2011         """
2012         String received in the 'init' state.
2013         """
2014         self._currentBox = AmpBox()
2015         return self.proto_key(string)
2016
2017
2018     def proto_key(self, string):
2019         """
2020         String received in the 'key' state.  If the key is empty, a complete
2021         box has been received.
2022         """
2023         if string:
2024             self._currentKey = string
2025             self.MAX_LENGTH = self._MAX_VALUE_LENGTH
2026             return 'value'
2027         else:
2028             self.boxReceiver.ampBoxReceived(self._currentBox)
2029             self._currentBox = None
2030             return 'init'
2031
2032
2033     def proto_value(self, string):
2034         """
2035         String received in the 'value' state.
2036         """
2037         self._currentBox[self._currentKey] = string
2038         self._currentKey = None
2039         self.MAX_LENGTH = self._MAX_KEY_LENGTH
2040         return 'key'
2041
2042
2043     def lengthLimitExceeded(self, length):
2044         """
2045         The key length limit was exceeded.  Disconnect the transport and make
2046         sure a meaningful exception is reported.
2047         """
2048         self._keyLengthLimitExceeded = True
2049         self.transport.loseConnection()
2050
2051
2052     def _lockForSwitch(self):
2053         """
2054         Lock this binary protocol so that no further boxes may be sent.  This
2055         is used when sending a request to switch underlying protocols.  You
2056         probably want to subclass ProtocolSwitchCommand rather than calling
2057         this directly.
2058         """
2059         self._locked = True
2060
2061
2062     def _unlockFromSwitch(self):
2063         """
2064         Unlock this locked binary protocol so that further boxes may be sent
2065         again.  This is used after an attempt to switch protocols has failed
2066         for some reason.
2067         """
2068         if self.innerProtocol is not None:
2069             raise ProtocolSwitched("Protocol already switched.  Cannot unlock.")
2070         self._locked = False
2071
2072
2073     def _prepareTLS(self, certificate, verifyAuthorities):
2074         """
2075         Used by StartTLSCommand to put us into the state where we don't
2076         actually send things that get sent, instead we buffer them.  see
2077         L{_sendBox}.
2078         """
2079         self._startingTLSBuffer = []
2080         if self.hostCertificate is not None:
2081             raise OnlyOneTLS(
2082                 "Previously authenticated connection between %s and %s "
2083                 "is trying to re-establish as %s" % (
2084                     self.hostCertificate,
2085                     self.peerCertificate,
2086                     (certificate, verifyAuthorities)))
2087
2088
2089     def _startTLS(self, certificate, verifyAuthorities):
2090         """
2091         Used by TLSBox to initiate the SSL handshake.
2092
2093         @param certificate: a L{twisted.internet.ssl.PrivateCertificate} for
2094         use locally.
2095
2096         @param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances
2097         representing certificate authorities which will verify our peer.
2098         """
2099         self.hostCertificate = certificate
2100         self._justStartedTLS = True
2101         if verifyAuthorities is None:
2102             verifyAuthorities = ()
2103         self.transport.startTLS(certificate.options(*verifyAuthorities))
2104         stlsb = self._startingTLSBuffer
2105         if stlsb is not None:
2106             self._startingTLSBuffer = None
2107             for box in stlsb:
2108                 self.sendBox(box)
2109
2110
2111     def _getPeerCertificate(self):
2112         if self.noPeerCertificate:
2113             return None
2114         return Certificate.peerFromTransport(self.transport)
2115     peerCertificate = property(_getPeerCertificate)
2116
2117
2118     def unhandledError(self, failure):
2119         """
2120         The buck stops here.  This error was completely unhandled, time to
2121         terminate the connection.
2122         """
2123         log.msg("Amp server or network failure "
2124                 "unhandled by client application:")
2125         log.err(failure)
2126         log.msg(
2127             "Dropping connection!  "
2128             "To avoid, add errbacks to ALL remote commands!")
2129         if self.transport is not None:
2130             self.transport.loseConnection()
2131
2132
2133     def _defaultStartTLSResponder(self):
2134         """
2135         The default TLS responder doesn't specify any certificate or anything.
2136
2137         From a security perspective, it's little better than a plain-text
2138         connection - but it is still a *bit* better, so it's included for
2139         convenience.
2140
2141         You probably want to override this by providing your own StartTLS.responder.
2142         """
2143         return {}
2144     StartTLS.responder(_defaultStartTLSResponder)
2145
2146
2147
2148 class AMP(BinaryBoxProtocol, BoxDispatcher,
2149           CommandLocator, SimpleStringLocator):
2150     """
2151     This protocol is an AMP connection.  See the module docstring for protocol
2152     details.
2153     """
2154
2155     _ampInitialized = False
2156
2157     def __init__(self, boxReceiver=None, locator=None):
2158         # For backwards compatibility.  When AMP did not separate parsing logic
2159         # (L{BinaryBoxProtocol}), request-response logic (L{BoxDispatcher}) and
2160         # command routing (L{CommandLocator}), it did not have a constructor.
2161         # Now it does, so old subclasses might have defined their own that did
2162         # not upcall.  If this flag isn't set, we'll call the constructor in
2163         # makeConnection before anything actually happens.
2164         self._ampInitialized = True
2165         if boxReceiver is None:
2166             boxReceiver = self
2167         if locator is None:
2168             locator = self
2169         BoxDispatcher.__init__(self, locator)
2170         BinaryBoxProtocol.__init__(self, boxReceiver)
2171
2172
2173     def locateResponder(self, name):
2174         """
2175         Unify the implementations of L{CommandLocator} and
2176         L{SimpleStringLocator} to perform both kinds of dispatch, preferring
2177         L{CommandLocator}.
2178         """
2179         firstResponder = CommandLocator.locateResponder(self, name)
2180         if firstResponder is not None:
2181             return firstResponder
2182         secondResponder = SimpleStringLocator.locateResponder(self, name)
2183         return secondResponder
2184
2185
2186     def __repr__(self):
2187         """
2188         A verbose string representation which gives us information about this
2189         AMP connection.
2190         """
2191         if self.innerProtocol is not None:
2192             innerRepr = ' inner %r' % (self.innerProtocol,)
2193         else:
2194             innerRepr = ''
2195         return '<%s%s at 0x%x>' % (
2196             self.__class__.__name__, innerRepr, unsignedID(self))
2197
2198
2199     def makeConnection(self, transport):
2200         """
2201         Emit a helpful log message when the connection is made.
2202         """
2203         if not self._ampInitialized:
2204             # See comment in the constructor re: backward compatibility.  I
2205             # should probably emit a deprecation warning here.
2206             AMP.__init__(self)
2207         # Save these so we can emit a similar log message in L{connectionLost}.
2208         self._transportPeer = transport.getPeer()
2209         self._transportHost = transport.getHost()
2210         log.msg("%s connection established (HOST:%s PEER:%s)" % (
2211                 self.__class__.__name__,
2212                 self._transportHost,
2213                 self._transportPeer))
2214         BinaryBoxProtocol.makeConnection(self, transport)
2215
2216
2217     def connectionLost(self, reason):
2218         """
2219         Emit a helpful log message when the connection is lost.
2220         """
2221         log.msg("%s connection lost (HOST:%s PEER:%s)" %
2222                 (self.__class__.__name__,
2223                  self._transportHost,
2224                  self._transportPeer))
2225         BinaryBoxProtocol.connectionLost(self, reason)
2226         self.transport = None
2227
2228
2229
2230 class _ParserHelper:
2231     """
2232     A box receiver which records all boxes received.
2233     """
2234     def __init__(self):
2235         self.boxes = []
2236
2237
2238     def getPeer(self):
2239         return 'string'
2240
2241
2242     def getHost(self):
2243         return 'string'
2244
2245     disconnecting = False
2246
2247
2248     def startReceivingBoxes(self, sender):
2249         """
2250         No initialization is required.
2251         """
2252
2253
2254     def ampBoxReceived(self, box):
2255         self.boxes.append(box)
2256
2257
2258     # Synchronous helpers
2259     def parse(cls, fileObj):
2260         """
2261         Parse some amp data stored in a file.
2262
2263         @param fileObj: a file-like object.
2264
2265         @return: a list of AmpBoxes encoded in the given file.
2266         """
2267         parserHelper = cls()
2268         bbp = BinaryBoxProtocol(boxReceiver=parserHelper)
2269         bbp.makeConnection(parserHelper)
2270         bbp.dataReceived(fileObj.read())
2271         return parserHelper.boxes
2272     parse = classmethod(parse)
2273
2274
2275     def parseString(cls, data):
2276         """
2277         Parse some amp data stored in a string.
2278
2279         @param data: a str holding some amp-encoded data.
2280
2281         @return: a list of AmpBoxes encoded in the given string.
2282         """
2283         return cls.parse(StringIO(data))
2284     parseString = classmethod(parseString)
2285
2286
2287
2288 parse = _ParserHelper.parse
2289 parseString = _ParserHelper.parseString
2290
2291 def _stringsToObjects(strings, arglist, proto):
2292     """
2293     Convert an AmpBox to a dictionary of python objects, converting through a
2294     given arglist.
2295
2296     @param strings: an AmpBox (or dict of strings)
2297
2298     @param arglist: a list of 2-tuples of strings and Argument objects, as
2299     described in L{Command.arguments}.
2300
2301     @param proto: an L{AMP} instance.
2302
2303     @return: the converted dictionary mapping names to argument objects.
2304     """
2305     objects = {}
2306     myStrings = strings.copy()
2307     for argname, argparser in arglist:
2308         argparser.fromBox(argname, myStrings, objects, proto)
2309     return objects
2310
2311
2312
2313 def _objectsToStrings(objects, arglist, strings, proto):
2314     """
2315     Convert a dictionary of python objects to an AmpBox, converting through a
2316     given arglist.
2317
2318     @param objects: a dict mapping names to python objects
2319
2320     @param arglist: a list of 2-tuples of strings and Argument objects, as
2321     described in L{Command.arguments}.
2322
2323     @param strings: [OUT PARAMETER] An object providing the L{dict}
2324     interface which will be populated with serialized data.
2325
2326     @param proto: an L{AMP} instance.
2327
2328     @return: The converted dictionary mapping names to encoded argument
2329     strings (identical to C{strings}).
2330     """
2331     myObjects = {}
2332     for (k, v) in objects.items():
2333         myObjects[k] = v
2334
2335     for argname, argparser in arglist:
2336         argparser.toBox(argname, strings, myObjects, proto)
2337     return strings
2338
2339
Note: See TracBrowser for help on using the browser.