root/trunk/twisted/protocols/amp.py

Revision 29159, 83.3 KB (checked in by exarkun, 3 months ago)

Add optional parameter to twisted.protocols.amp.ListOf

Author: faldridge
Reviewer: exarkun
Fixes: #4474

Let ListOf arguments be optional in the usual way.

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