1
# -*- test-case-name: twisted.test.test_amp -*-
2
# Copyright (c) 2005 Divmod, Inc.
3
# Copyright (c) 2007 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
This module implements AMP, the Asynchronous Messaging Protocol.
9
AMP is a protocol for sending multiple asynchronous request/response pairs over
10
the same connection. Requests and responses are both collections of key/value
13
AMP is a very simple protocol which is not an application. This module is a
14
"protocol construction kit" of sorts; it attempts to be the simplest wire-level
15
implementation of Deferreds. AMP provides the following base-level features:
17
- Asynchronous request/response handling (hence the name)
19
- Requests and responses are both key/value pairs
21
- Binary transfer of all data: all data is length-prefixed. Your
22
application will never need to worry about quoting.
24
- Command dispatching (like HTTP Verbs): the protocol is extensible, and
25
multiple AMP sub-protocols can be grouped together easily.
27
The protocol implementation also provides a few additional features which are
28
not part of the core wire protocol, but are nevertheless very useful:
30
- Tight TLS integration, with an included StartTLS command.
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
39
Using AMP with Twisted is simple. Each message is a command, with a response.
40
You begin by defining a command type. Commands specify their input and output
41
in terms of the types that they expect to see in the request and response
42
key-value pairs. Here's an example of a command that adds two integers, 'a'
45
class Sum(amp.Command):
46
arguments = [('a', amp.Integer()),
48
response = [('total', amp.Integer())]
50
Once you have specified a command, you need to make it part of a protocol, and
51
define a responder for it. Here's a 'JustSum' protocol that includes a
52
responder for our 'Sum' command::
54
class JustSum(amp.AMP):
57
print 'Did a sum: %d + %d = %d' % (a, b, total)
58
return {'total': total}
61
Later, when you want to actually do a sum, the following expression will return
62
a Deferred which will fire with the result::
64
ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
65
lambda p: p.callRemote(Sum, a=13, b=81)).addCallback(
66
lambda result: result['total'])
68
You can also define the propagation of specific errors in AMP. For example,
69
for the slightly more complicated case of division, we might have to deal with
72
class Divide(amp.Command):
73
arguments = [('numerator', amp.Integer()),
74
('denominator', amp.Integer())]
75
response = [('result', amp.Float())]
76
errors = {ZeroDivisionError: 'ZERO_DIVISION'}
78
The 'errors' mapping here tells AMP that if a responder to Divide emits a
79
L{ZeroDivisionError}, then the other side should be informed that an error of
80
the type 'ZERO_DIVISION' has occurred. Writing a responder which takes
81
advantage of this is very simple - just raise your exception normally::
83
class JustDivide(amp.AMP):
84
def divide(self, numerator, denominator):
85
result = numerator / denominator
86
print 'Divided: %d / %d = %d' % (numerator, denominator, total)
87
return {'result': result}
88
Divide.responder(divide)
90
On the client side, the errors mapping will be used to determine what the
91
'ZERO_DIVISION' error means, and translated into an asynchronous exception,
92
which can be handled normally as any L{Deferred} would be::
95
result.trap(ZeroDivisionError)
96
print "Divided by zero: returning INF"
98
ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
99
lambda p: p.callRemote(Divide, numerator=1234,
101
).addErrback(trapZero)
103
For a complete, runnable example of both of these commands, see the files in
104
the Twisted repository::
106
doc/core/examples/ampserver.py
107
doc/core/examples/ampclient.py
109
On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and
110
values, and empty keys to separate messages::
112
<2-byte length><key><2-byte length><value>
113
<2-byte length><key><2-byte length><value>
115
<2-byte length><key><2-byte length><value>
116
<NUL><NUL> # Empty Key == End of Message
118
And so on. Because it's tedious to refer to lengths and NULs constantly, the
119
documentation will refer to packets as if they were newline delimited, like
123
C: _ask: ef639e5c892ccb54
127
S: _answer: ef639e5c892ccb54
132
Values are limited to the maximum encodable size in a 16-bit length, 65535
135
Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes.
136
Note that we still use 2-byte lengths to encode keys. This small redundancy
137
has several features:
139
- If an implementation becomes confused and starts emitting corrupt data,
140
or gets keys confused with values, many common errors will be
141
signalled immediately instead of delivering obviously corrupt packets.
143
- A single NUL will separate every key, and a double NUL separates
144
messages. This provides some redundancy when debugging traffic dumps.
146
- NULs will be present at regular intervals along the protocol, providing
147
some padding for otherwise braindead C implementations of the protocol,
148
so that <stdio.h> string functions will see the NUL and stop.
150
- This makes it possible to run an AMP server on a port also used by a
151
plain-text protocol, and easily distinguish between non-AMP clients (like
152
web browsers) which issue non-NUL as the first byte, and AMP clients,
153
which always issue NUL as the first byte.
159
import types, warnings
161
from cStringIO import StringIO
162
from struct import pack
164
from zope.interface import Interface, implements
166
from twisted.python.reflect import accumulateClassDict
167
from twisted.python.failure import Failure
168
from twisted.python import log, filepath
170
from twisted.internet.main import CONNECTION_LOST
171
from twisted.internet.error import ConnectionLost
172
from twisted.internet.defer import Deferred, maybeDeferred, fail
173
from twisted.protocols.basic import Int16StringReceiver, StatefulStringProtocol
175
#from twisted.internet._sslverify import problemsFromTransport
177
# I'd like this to use the exposed public API, but for some reason, when it was
178
# moved, these names were not exposed by internet.ssl.
180
#from twisted.internet.ssl import CertificateOptions, Certificate, DN, KeyPair
186
ERROR_CODE = '_error_code'
187
ERROR_DESCRIPTION = '_error_description'
188
UNKNOWN_ERROR_CODE = 'UNKNOWN'
189
UNHANDLED_ERROR_CODE = 'UNHANDLED'
191
MAX_KEY_LENGTH = 0xff
192
MAX_VALUE_LENGTH = 0xffff
195
class IBoxSender(Interface):
197
A transport which can send L{AmpBox} objects.
204
@raise ProtocolSwitched: if the underlying protocol has been
207
@raise ConnectionLost: if the underlying connection has already been
211
def unhandledError(failure):
213
An unhandled error occurred in response to a box. Log it
216
@param failure: a L{Failure} describing the error that occurred.
221
class IBoxReceiver(Interface):
223
An application object which can receive L{AmpBox} objects and dispatch them
227
def startReceivingBoxes(boxSender):
229
The L{ampBoxReceived} method will start being called; boxes may be
230
responded to by responding to the given L{IBoxSender}.
232
@param boxSender: an L{IBoxSender} provider.
236
def ampBoxReceived(box):
238
A box was received from the transport; dispatch it appropriately.
242
def stopReceivingBoxes(reason):
244
No further boxes will be received on this connection.
246
@type reason: L{Failure}
251
class IResponderLocator(Interface):
253
An application object which can look up appropriate responder methods for
257
def locateResponder(self, name):
259
Locate a responder method appropriate for the named command.
261
@param name: the wire-level name (commandName) of the AMP command to be
264
@return: a 1-argument callable that takes an L{AmpBox} with argument
265
values for the given command, and returns an L{AmpBox} containing
266
argument values for the named command, or a L{Deferred} that fires the
272
class AmpError(Exception):
274
Base class of all Amp-related exceptions.
279
class ProtocolSwitched(Exception):
281
Connections which have been switched to other protocols can no longer
282
accept traffic at the AMP level. This is raised when you try to send it.
287
class OnlyOneTLS(AmpError):
289
This is an implementation limitation; TLS may only be started once per
295
class NoEmptyBoxes(AmpError):
297
You can't have empty boxes on the connection. This is raised when you
298
receive or attempt to send one.
303
class InvalidSignature(AmpError):
305
You didn't pass all the required arguments.
310
class TooLong(AmpError):
312
One of the protocol's length limitations was violated.
314
@ivar isKey: true if the string being encoded in a key position, false if
315
it was in a value position.
317
@ivar isLocal: Was the string encoded locally, or received too long from
318
the network? (It's only physically possible to encode "too long" values on
319
the network for keys.)
321
@ivar value: The string that was too long.
323
@ivar keyName: If the string being encoded was in a value position, what
324
key was it being encoded for?
327
def __init__(self, isKey, isLocal, value, keyName=None):
328
AmpError.__init__(self)
330
self.isLocal = isLocal
332
self.keyName = keyName
336
hdr = self.isKey and "key" or "value"
338
hdr += ' ' + repr(self.keyName)
339
lcl = self.isLocal and "local" or "remote"
340
return "%s %s too long: %d" % (lcl, hdr, len(self.value))
344
class BadLocalReturn(AmpError):
346
A bad value was returned from a local command; we were unable to coerce it.
348
def __init__(self, message, enclosed):
349
AmpError.__init__(self)
350
self.message = message
351
self.enclosed = enclosed
355
return self.message + " " + self.enclosed.getBriefTraceback()
361
class RemoteAmpError(AmpError):
363
This error indicates that something went wrong on the remote end of the
364
connection, and the error was serialized and transmitted to you.
366
def __init__(self, errorCode, description, fatal=False, local=None):
367
"""Create a remote error with an error code and description.
369
@param errorCode: the AMP error code of this error.
371
@param description: some text to show to the user.
373
@param fatal: a boolean, true if this error should terminate the
376
@param local: a local Failure, if one exists.
379
localwhat = ' (local)'
380
othertb = local.getBriefTraceback()
384
Exception.__init__(self, "Code<%s>%s: %s%s" % (
385
errorCode, localwhat,
386
description, othertb))
388
self.errorCode = errorCode
389
self.description = description
394
class UnknownRemoteError(RemoteAmpError):
396
This means that an error whose type we can't identify was raised from the
399
def __init__(self, description):
400
errorCode = UNKNOWN_ERROR_CODE
401
RemoteAmpError.__init__(self, errorCode, description)
405
class MalformedAmpBox(AmpError):
407
This error indicates that the wire-level protocol was malformed.
412
class UnhandledCommand(AmpError):
414
A command received via amp could not be dispatched.
419
class IncompatibleVersions(AmpError):
421
It was impossible to negotiate a compatible version of the protocol with
422
the other end of the connection.
426
PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand}
430
I am a packet in the AMP protocol, much like a regular str:str dictionary.
432
__slots__ = [] # be like a regular dictionary, don't magically
433
# acquire a __dict__...
438
Return another AmpBox just like me.
440
newBox = self.__class__()
447
Convert me into a wire-encoded string.
449
@return: a str encoded according to the rules described in the module
457
if len(k) > MAX_KEY_LENGTH:
458
raise TooLong(True, True, k, None)
459
if len(v) > MAX_VALUE_LENGTH:
460
raise TooLong(False, True, v, k)
462
w(pack("!H", len(kv)))
468
def _sendTo(self, proto):
470
Serialize and send this box to a Amp instance. By the time it is being
471
sent, several keys are required. I must have exactly ONE of::
477
If the '_ask' key is set, then the '_command' key must also be
480
@param proto: an AMP instance.
485
return 'AmpBox(%s)' % (dict.__repr__(self),)
491
class QuitBox(AmpBox):
493
I am an AmpBox that, upon being sent, terminates the connection.
499
return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),)
502
def _sendTo(self, proto):
504
Immediately call loseConnection after sending.
506
super(QuitBox, self)._sendTo(proto)
507
proto.transport.loseConnection()
511
class _SwitchBox(AmpBox):
513
Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets
514
up state for the protocol to switch.
517
# DON'T set __slots__ here; we do have an attribute.
519
def __init__(self, innerProto, **kw):
521
Create a _SwitchBox with the protocol to switch to after being sent.
523
@param innerProto: the protocol instance to switch to.
524
@type innerProto: an IProtocol provider.
526
super(_SwitchBox, self).__init__(**kw)
527
self.innerProto = innerProto
531
return '_SwitchBox(%r, **%s)' % (self.innerProto,
532
dict.__repr__(self),)
535
def _sendTo(self, proto):
537
Send me; I am the last box on the connection. All further traffic will be
538
over the new protocol.
540
super(_SwitchBox, self)._sendTo(proto)
541
proto._lockForSwitch()
542
proto._switchTo(self.innerProto)
548
A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es,
549
both incoming and outgoing, to their appropriate destinations.
551
Outgoing commands are converted into L{Deferred}s and outgoing boxes, and
552
associated tracking state to fire those L{Deferred} when '_answer' boxes
553
come back. Incoming '_answer' and '_error' boxes are converted into
554
callbacks and errbacks on those L{Deferred}s, respectively.
556
Incoming '_ask' boxes are converted into method calls on a supplied method
559
@ivar _outstandingRequests: a dictionary mapping request IDs to
560
L{Deferred}s which were returned for those requests.
562
@ivar locator: an object with a L{locateResponder} method that locates a
563
responder function that takes a Box and returns a result (either a Box or a
564
Deferred which fires one).
566
@ivar boxSender: an object which can send boxes, via the L{_sendBox}
567
method, such as an L{AMP} instance.
568
@type boxSender: L{IBoxSender}
571
implements(IBoxReceiver)
573
_failAllReason = None
574
_outstandingRequests = None
578
def __init__(self, locator):
579
self._outstandingRequests = {}
580
self.locator = locator
583
def startReceivingBoxes(self, boxSender):
585
The given boxSender is going to start calling boxReceived on this
588
@param boxSender: The L{IBoxSender} to send command responses to.
590
self.boxSender = boxSender
593
def stopReceivingBoxes(self, reason):
595
No further boxes will be received here. Terminate all currently
596
oustanding command deferreds with the given reason.
598
self.failAllOutgoing(reason)
601
def failAllOutgoing(self, reason):
603
Call the errback on all outstanding requests awaiting responses.
605
@param reason: the Failure instance to pass to those errbacks.
607
self._failAllReason = reason
608
OR = self._outstandingRequests.items()
609
self._outstandingRequests = None # we can never send another request
610
for key, value in OR:
611
value.errback(reason)
616
Generate protocol-local serial numbers for _ask keys.
618
@return: a string that has not yet been used on this connection.
621
return '%x' % (self._counter,)
624
def _sendBoxCommand(self, command, box, requiresAnswer=True):
626
Send a command across the wire with the given C{amp.Box}.
628
Mutate the given box to give it any additional keys (_command, _ask)
629
required for the command and request/response machinery, then send it.
631
If requiresAnswer is True, returns a C{Deferred} which fires when a
632
response is received. The C{Deferred} is fired with an C{amp.Box} on
633
success, or with an C{amp.RemoteAmpError} if an error is received.
635
If the Deferred fails and the error is not handled by the caller of
636
this method, the failure will be logged and the connection dropped.
638
@param command: a str, the name of the command to issue.
640
@param box: an AmpBox with the arguments for the command.
642
@param requiresAnswer: a boolean. Defaults to True. If True, return a
643
Deferred which will fire when the other side responds to this command.
644
If False, return None and do not ask the other side for acknowledgement.
646
@return: a Deferred which fires the AmpBox that holds the response to
647
this command, or None, as specified by requiresAnswer.
649
@raise ProtocolSwitched: if the protocol has been switched.
651
if self._failAllReason is not None:
652
return fail(self._failAllReason)
653
box[COMMAND] = command
654
tag = self._nextTag()
657
box._sendTo(self.boxSender)
659
result = self._outstandingRequests[tag] = Deferred()
665
def callRemoteString(self, command, requiresAnswer=True, **kw):
667
This is a low-level API, designed only for optimizing simple messages
668
for which the overhead of parsing is too great.
670
@param command: a str naming the command.
672
@param kw: arguments to the amp box.
674
@param requiresAnswer: a boolean. Defaults to True. If True, return a
675
Deferred which will fire when the other side responds to this command.
676
If False, return None and do not ask the other side for acknowledgement.
678
@return: a Deferred which fires the AmpBox that holds the response to
679
this command, or None, as specified by requiresAnswer.
682
return self._sendBoxCommand(command, box)
685
def callRemote(self, commandType, *a, **kw):
687
This is the primary high-level API for sending messages via AMP. Invoke it
688
with a command and appropriate arguments to send a message to this
691
@param commandType: a subclass of Command.
692
@type commandType: L{type}
694
@param a: Positional (special) parameters taken by the command.
695
Positional parameters will typically not be sent over the wire. The
696
only command included with AMP which uses positional parameters is
697
L{ProtocolSwitchCommand}, which takes the protocol that will be
698
switched to as its first argument.
700
@param kw: Keyword arguments taken by the command. These are the
701
arguments declared in the command's 'arguments' attribute. They will
702
be encoded and sent to the peer as arguments for the L{commandType}.
704
@return: If L{commandType} has a C{requiresAnswer} attribute set to
705
L{False}, then return L{None}. Otherwise, return a L{Deferred} which
706
fires with a dictionary of objects representing the result of this
707
call. Additionally, this L{Deferred} may fail with an exception
708
representing a connection failure, with L{UnknownRemoteError} if the
709
other end of the connection fails for an unknown reason, or with any
710
error specified as a key in L{commandType}'s C{errors} dictionary.
713
# XXX this takes command subclasses and not command objects on purpose.
714
# There's really no reason to have all this back-and-forth between
715
# command objects and the protocol, and the extra object being created
716
# (the Command instance) is pointless. Command is kind of like
717
# Interface, and should be more like it.
719
# In other words, the fact that commandType is instantiated here is an
720
# implementation detail. Don't rely on it.
722
co = commandType(*a, **kw)
723
return co._doCommand(self)
726
def unhandledError(self, failure):
728
This is a terminal callback called after application code has had a
729
chance to quash any errors.
731
return self.boxSender.unhandledError(failure)
734
def _answerReceived(self, box):
736
An AMP box was received that answered a command previously sent with
739
@param box: an AmpBox with a value for its L{ANSWER} key.
741
question = self._outstandingRequests.pop(box[ANSWER])
742
question.addErrback(self.unhandledError)
743
question.callback(box)
746
def _errorReceived(self, box):
748
An AMP box was received that answered a command previously sent with
749
L{callRemote}, with an error.
751
@param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE},
752
and L{ERROR_DESCRIPTION} keys.
754
question = self._outstandingRequests.pop(box[ERROR])
755
question.addErrback(self.unhandledError)
756
errorCode = box[ERROR_CODE]
757
description = box[ERROR_DESCRIPTION]
758
if errorCode in PROTOCOL_ERRORS:
759
exc = PROTOCOL_ERRORS[errorCode](errorCode, description)
761
exc = RemoteAmpError(errorCode, description)
762
question.errback(Failure(exc))
765
def _commandReceived(self, box):
767
@param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK}
771
def formatAnswer(answerBox):
772
answerBox[ANSWER] = box[ASK]
774
def formatError(error):
775
if error.check(RemoteAmpError):
776
code = error.value.errorCode
777
desc = error.value.description
778
if error.value.fatal:
784
log.err(error) # here is where server-side logging happens
785
# if the error isn't handled
786
code = UNKNOWN_ERROR_CODE
787
desc = "Unknown Error"
788
errorBox[ERROR] = box[ASK]
789
errorBox[ERROR_DESCRIPTION] = desc
790
errorBox[ERROR_CODE] = code
792
deferred = self.dispatchCommand(box)
794
deferred.addCallbacks(formatAnswer, formatError)
795
deferred.addCallback(self._safeEmit)
796
deferred.addErrback(self.unhandledError)
799
def ampBoxReceived(self, box):
801
An AmpBox was received, representing a command, or an answer to a
802
previously issued command (either successful or erroneous). Respond to
803
it according to its contents.
805
@param box: an AmpBox
807
@raise NoEmptyBoxes: when a box is received that does not contain an
808
'_answer', '_command' / '_ask', or '_error' key; i.e. one which does not
809
fit into the command / response protocol defined by AMP.
812
self._answerReceived(box)
814
self._errorReceived(box)
816
self._commandReceived(box)
818
raise NoEmptyBoxes(box)
821
def _safeEmit(self, aBox):
823
Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors
824
which cannot be usefully handled.
827
aBox._sendTo(self.boxSender)
828
except (ProtocolSwitched, ConnectionLost):
832
def dispatchCommand(self, box):
834
A box with a _command key was received.
836
Dispatch it to a local handler call it.
838
@param proto: an AMP instance.
839
@param box: an AmpBox to be dispatched.
842
responder = self.locator.locateResponder(cmd)
843
if responder is None:
844
return fail(RemoteAmpError(
845
UNHANDLED_ERROR_CODE,
846
"Unhandled Command: %r" % (cmd,),
848
local=Failure(UnhandledCommand())))
849
return maybeDeferred(responder, box)
853
class CommandLocator:
855
A L{CommandLocator} is a collection of responders to AMP L{Command}s, with
856
the help of the L{Command.responder} decorator.
859
class __metaclass__(type):
861
This metaclass keeps track of all of the Command.responder-decorated
862
methods defined since the last CommandLocator subclass was defined. It
863
assumes (usually correctly, but unfortunately not necessarily so) that
864
those commands responders were all declared as methods of the class
865
being defined. Note that this list can be incorrect if users use the
866
Command.responder decorator outside the context of a CommandLocator
869
The Command.responder decorator explicitly cooperates with this
873
_currentClassCommands = []
874
def __new__(cls, name, bases, attrs):
875
commands = cls._currentClassCommands[:]
876
cls._currentClassCommands[:] = []
877
cd = attrs['_commandDispatch'] = {}
879
cls._grabFromBase(cd, base)
880
for commandClass, responderFunc in commands:
881
cd[commandClass.commandName] = (commandClass, responderFunc)
882
subcls = type.__new__(cls, name, bases, attrs)
884
subcls.lookupFunction != CommandLocator.lookupFunction)):
885
def locateResponder(self, name):
887
"Override locateResponder, not lookupFunction.",
888
category=PendingDeprecationWarning,
890
return self.lookupFunction(name)
891
subcls.locateResponder = locateResponder
894
def _grabFromBase(cls, cd, base):
895
if hasattr(base, "_commandDispatch"):
896
cd.update(base._commandDispatch)
897
for subbase in base.__bases__:
898
cls._grabFromBase(cd, subbase)
899
_grabFromBase = classmethod(_grabFromBase)
901
implements(IResponderLocator)
904
def _wrapWithSerialization(self, aCallable, command):
906
Wrap aCallable with its command's argument de-serialization
907
and result serialization logic.
909
@param aCallable: a callable with a 'command' attribute, designed to be
910
called with keyword arguments.
912
@param command: the command class whose serialization to use.
914
@return: a 1-arg callable which, when invoked with an AmpBox, will
915
deserialize the argument list and invoke appropriate user code for the
916
callable's command, returning a Deferred which fires with the result or
920
kw = command.parseArguments(box, self)
921
def checkKnownErrors(error):
922
key = error.trap(*command.allErrors)
923
code = command.allErrors[key]
924
desc = str(error.value)
925
return Failure(RemoteAmpError(
926
code, desc, key in command.fatalErrors, local=error))
927
def makeResponseFor(objects):
929
return command.makeResponse(objects, self)
931
# let's helpfully log this.
932
originalFailure = Failure()
933
raise BadLocalReturn(
934
"%r returned %r and %r could not serialize it" % (
939
return maybeDeferred(aCallable, **kw).addCallback(
940
makeResponseFor).addErrback(
945
def lookupFunction(self, name):
947
Deprecated synonym for L{locateResponder}
949
if self.__class__.lookupFunction != CommandLocator.lookupFunction:
950
return CommandLocator.locateResponder(self, name)
952
warnings.warn("Call locateResponder, not lookupFunction.",
953
category=PendingDeprecationWarning,
955
return self.locateResponder(name)
958
def locateResponder(self, name):
960
Locate a callable to invoke when executing the named command.
962
@param name: the normalized name (from the wire) of the command.
964
@return: a 1-argument function that takes a Box and returns a box or a
965
Deferred which fires a Box, for handling the command identified by the
966
given name, or None, if no appropriate responder can be found.
968
# Try to find a high-level method to invoke, and if we can't find one,
969
# fall back to a low-level one.
970
cd = self._commandDispatch
972
commandClass, responderFunc = cd[name]
973
responderMethod = types.MethodType(
974
responderFunc, self, self.__class__)
975
return self._wrapWithSerialization(responderMethod, commandClass)
979
class SimpleStringLocator(object):
981
Implement the L{locateResponder} method to do simple, string-based
985
implements(IResponderLocator)
987
baseDispatchPrefix = 'amp_'
989
def locateResponder(self, name):
991
Locate a callable to invoke when executing the named command.
993
@return: a function with the name C{"amp_" + name} on L{self}, or None
994
if no such function exists. This function will then be called with the
995
L{AmpBox} itself as an argument.
997
@param name: the normalized name (from the wire) of the command.
999
fName = self.baseDispatchPrefix + (name.upper())
1000
return getattr(self, fName, None)
1005
'and', 'del', 'for', 'is', 'raise', 'assert', 'elif', 'from', 'lambda',
1006
'return', 'break', 'else', 'global', 'not', 'try', 'class', 'except',
1007
'if', 'or', 'while', 'continue', 'exec', 'import', 'pass', 'yield',
1008
'def', 'finally', 'in', 'print']
1012
def _wireNameToPythonIdentifier(key):
1014
(Private) Normalize an argument name from the wire for use with Python
1015
code. If the return value is going to be a python keyword it will be
1016
capitalized. If it contains any dashes they will be replaced with
1019
The rationale behind this method is that AMP should be an inherently
1020
multi-language protocol, so message keys may contain all manner of bizarre
1021
bytes. This is not a complete solution; there are still forms of arguments
1022
that this implementation will be unable to parse. However, Python
1023
identifiers share a huge raft of properties with identifiers from many
1024
other languages, so this is a 'good enough' effort for now. We deal
1025
explicitly with dashes because that is the most likely departure: Lisps
1026
commonly use dashes to separate method names, so protocols initially
1027
implemented in a lisp amp dialect may use dashes in argument or command
1030
@param key: a str, looking something like 'foo-bar-baz' or 'from'
1032
@return: a str which is a valid python identifier, looking something like
1033
'foo_bar_baz' or 'From'.
1035
lkey = key.replace("-", "_")
1036
if lkey in PYTHON_KEYWORDS:
1044
Base-class of all objects that take values from Amp packets and convert
1045
them into objects for Python functions.
1050
def __init__(self, optional=False):
1054
@param optional: a boolean indicating whether this argument can be
1055
omitted in the protocol.
1057
self.optional = optional
1060
def retrieve(self, d, name, proto):
1062
Retrieve the given key from the given dictionary, removing it if found.
1064
@param d: a dictionary.
1066
@param name: a key in L{d}.
1068
@param proto: an instance of an AMP.
1070
@raise KeyError: if I am not optional and no value was found.
1076
if value is not None:
1083
def fromBox(self, name, strings, objects, proto):
1085
Populate an 'out' dictionary with mapping names to Python values
1086
decoded from an 'in' AmpBox mapping strings to string values.
1088
@param name: the argument name to retrieve
1091
@param strings: The AmpBox to read string(s) from, a mapping of
1092
argument names to string values.
1093
@type strings: AmpBox
1095
@param objects: The dictionary to write object(s) to, a mapping of
1096
names to Python objects.
1099
@param proto: an AMP instance.
1101
st = self.retrieve(strings, name, proto)
1102
nk = _wireNameToPythonIdentifier(name)
1103
if self.optional and st is None:
1106
objects[nk] = self.fromStringProto(st, proto)
1109
def toBox(self, name, strings, objects, proto):
1111
Populate an 'out' AmpBox with strings encoded from an 'in' dictionary
1112
mapping names to Python values.
1114
@param name: the argument name to retrieve
1117
@param strings: The AmpBox to write string(s) to, a mapping of
1118
argument names to string values.
1119
@type strings: AmpBox
1121
@param objects: The dictionary to read object(s) from, a mapping of
1122
names to Python objects.
1126
@param proto: the protocol we are converting for.
1129
obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto)
1130
if self.optional and obj is None:
1131
# strings[name] = None
1134
strings[name] = self.toStringProto(obj, proto)
1137
def fromStringProto(self, inString, proto):
1139
Convert a string to a Python value.
1141
@param inString: the string to convert.
1143
@param proto: the protocol we are converting for.
1146
@return: a Python object.
1148
return self.fromString(inString)
1151
def toStringProto(self, inObject, proto):
1153
Convert a Python object to a string.
1155
@param inObject: the object to convert.
1157
@param proto: the protocol we are converting for.
1160
return self.toString(inObject)
1163
def fromString(self, inString):
1165
Convert a string to a Python object. Subclasses must implement this.
1167
@param inString: the string to convert.
1170
@return: the decoded value from inString
1174
def toString(self, inObject):
1176
Convert a Python object into a string for passing over the network.
1178
@param inObject: an object of the type that this Argument is intended
1181
@return: the wire encoding of inObject
1187
class Integer(Argument):
1189
Convert to and from 'int'.
1192
def toString(self, inObject):
1193
return str(int(inObject))
1197
class String(Argument):
1199
Don't do any conversion at all; just pass through 'str'.
1201
def toString(self, inObject):
1205
def fromString(self, inString):
1210
class Float(Argument):
1212
Encode floating-point values on the wire as their repr.
1219
class Boolean(Argument):
1221
Encode True or False as "True" or "False" on the wire.
1223
def fromString(self, inString):
1224
if inString == 'True':
1226
elif inString == 'False':
1229
raise TypeError("Bad boolean value: %r" % (inString,))
1232
def toString(self, inObject):
1240
class Unicode(String):
1242
Encode a unicode string on the wire as UTF-8.
1245
def toString(self, inObject):
1246
# assert isinstance(inObject, unicode)
1247
return String.toString(self, inObject.encode('utf-8'))
1250
def fromString(self, inString):
1251
# assert isinstance(inString, str)
1252
return String.fromString(self, inString).decode('utf-8')
1256
class Path(Unicode):
1258
Encode and decode L{filepath.FilePath} instances as paths on the wire.
1260
This is really intended for use with subprocess communication tools:
1261
exchanging pathnames on different machines over a network is not generally
1262
meaningful, but neither is it disallowed; you can use this to communicate
1263
about NFS paths, for example.
1265
def fromString(self, inString):
1266
return filepath.FilePath(Unicode.fromString(self, inString))
1269
def toString(self, inObject):
1270
return Unicode.toString(self, inObject.path)
1274
class AmpList(Argument):
1276
Convert a list of dictionaries into a list of AMP boxes on the wire.
1278
For example, if you want to pass::
1280
[{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}]
1282
You might use an AmpList like this in your arguments or response list::
1284
AmpList([('a', Integer()),
1287
def __init__(self, subargs):
1291
@param subargs: a list of 2-tuples of ('name', argument) describing the
1292
schema of the dictionaries in the sequence of amp boxes.
1294
self.subargs = subargs
1297
def fromStringProto(self, inString, proto):
1298
boxes = parseString(inString)
1299
values = [_stringsToObjects(box, self.subargs, proto)
1304
def toStringProto(self, inObject, proto):
1305
return ''.join([_objectsToStrings(
1306
objects, self.subargs, Box(), proto
1307
).serialize() for objects in inObject])
1311
Subclass me to specify an AMP Command.
1313
@cvar arguments: A list of 2-tuples of (name, Argument-subclass-instance),
1314
specifying the names and values of the parameters which are required for
1317
@cvar response: A list like L{arguments}, but instead used for the return
1320
@cvar errors: A mapping of subclasses of L{Exception} to wire-protocol tags
1321
for errors represented as L{str}s. Responders which raise keys from this
1322
dictionary will have the error translated to the corresponding tag on the
1323
wire. Invokers which receive Deferreds from invoking this command with
1324
L{AMP.callRemote} will potentially receive Failures with keys from this
1325
mapping as their value. This mapping is inherited; if you declare a
1326
command which handles C{FooError} as 'FOO_ERROR', then subclass it and
1327
specify C{BarError} as 'BAR_ERROR', responders to the subclass may raise
1328
either C{FooError} or C{BarError}, and invokers must be able to deal with
1329
either of those exceptions.
1331
@cvar fatalErrors: like 'errors', but errors in this list will always
1332
terminate the connection, despite being of a recognizable error type.
1334
@cvar commandType: The type of Box used to issue commands; useful only for
1335
protocol-modifying behavior like startTLS or protocol switching. Defaults
1336
to a plain vanilla L{Box}.
1338
@cvar responseType: The type of Box used to respond to this command; only
1339
useful for protocol-modifying behavior like startTLS or protocol switching.
1340
Defaults to a plain vanilla L{Box}.
1342
@ivar requiresAnswer: a boolean; defaults to True. Set it to False on your
1343
subclass if you want callRemote to return None. Note: this is a hint only
1344
to the client side of the protocol. The return-type of a command responder
1345
method must always be a dictionary adhering to the contract specified by
1346
L{response}, because clients are always free to request a response if they
1350
class __metaclass__(type):
1352
Metaclass hack to establish reverse-mappings for 'errors' and
1353
'fatalErrors' as class vars.
1355
def __new__(cls, name, bases, attrs):
1356
re = attrs['reverseErrors'] = {}
1357
er = attrs['allErrors'] = {}
1358
if 'commandName' not in attrs:
1359
attrs['commandName'] = name
1360
newtype = type.__new__(cls, name, bases, attrs)
1363
accumulateClassDict(newtype, 'errors', errors)
1364
accumulateClassDict(newtype, 'fatalErrors', fatalErrors)
1365
for v, k in errors.iteritems():
1368
for v, k in fatalErrors.iteritems():
1382
requiresAnswer = True
1385
def __init__(self, **kw):
1387
Create an instance of this command with specified values for its
1390
@param kw: a dict containing an appropriate value for each name
1391
specified in the L{arguments} attribute of my class.
1393
@raise InvalidSignature: if you forgot any required arguments.
1395
self.structured = kw
1396
givenArgs = kw.keys()
1398
for name, arg in self.arguments:
1399
pythonName = _wireNameToPythonIdentifier(name)
1400
if pythonName not in givenArgs and not arg.optional:
1401
forgotten.append(pythonName)
1403
raise InvalidSignature("forgot %s for %s" % (
1404
', '.join(forgotten), self.commandName))
1408
def makeResponse(cls, objects, proto):
1410
Serialize a mapping of arguments using this L{Command}'s
1413
@param objects: a dict with keys matching the names specified in
1414
self.response, having values of the types that the Argument objects in
1415
self.response can format.
1417
@param proto: an L{AMP}.
1419
@return: an L{AmpBox}.
1421
return _objectsToStrings(objects, cls.response, cls.responseType(),
1423
makeResponse = classmethod(makeResponse)
1426
def makeArguments(cls, objects, proto):
1428
Serialize a mapping of arguments using this L{Command}'s
1431
@param objects: a dict with keys similar to the names specified in
1432
self.arguments, having values of the types that the Argument objects in
1433
self.arguments can parse.
1435
@param proto: an L{AMP}.
1437
@return: An instance of this L{Command}'s C{commandType}.
1439
return _objectsToStrings(objects, cls.arguments, cls.commandType(),
1441
makeArguments = classmethod(makeArguments)
1444
def parseResponse(cls, box, protocol):
1446
Parse a mapping of serialized arguments using this
1447
L{Command}'s response schema.
1449
@param box: A mapping of response-argument names to the
1450
serialized forms of those arguments.
1451
@param protocol: The L{AMP} protocol.
1453
@return: A mapping of response-argument names to the parsed
1456
return _stringsToObjects(box, cls.response, protocol)
1457
parseResponse = classmethod(parseResponse)
1460
def parseArguments(cls, box, protocol):
1462
Parse a mapping of serialized arguments using this
1463
L{Command}'s argument schema.
1465
@param box: A mapping of argument names to the seralized forms
1467
@param protocol: The L{AMP} protocol.
1469
@return: A mapping of argument names to the parsed forms.
1471
return _stringsToObjects(box, cls.arguments, protocol)
1472
parseArguments = classmethod(parseArguments)
1475
def responder(cls, methodfunc):
1477
Declare a method to be a responder for a particular command.
1479
This is a decorator.
1483
class MyCommand(Command):
1484
arguments = [('a', ...), ('b', ...)]
1487
def myFunMethod(self, a, b):
1489
MyCommand.responder(myFunMethod)
1491
Notes: Although decorator syntax is not used within Twisted, this
1492
function returns its argument and is therefore safe to use with
1495
This is not thread safe. Don't declare AMP subclasses in other
1496
threads. Don't declare responders outside the scope of AMP subclasses;
1497
the behavior is undefined.
1499
@param methodfunc: A function which will later become a method, which
1500
has a keyword signature compatible with this command's L{argument} list
1501
and returns a dictionary with a set of keys compatible with this
1502
command's L{response} list.
1504
@return: the methodfunc parameter.
1506
CommandLocator._currentClassCommands.append((cls, methodfunc))
1508
responder = classmethod(responder)
1511
# Our only instance method
1512
def _doCommand(self, proto):
1514
Encode and send this Command to the given protocol.
1516
@param proto: an AMP, representing the connection to send to.
1518
@return: a Deferred which will fire or error appropriately when the
1519
other side responds to the command (or error if the connection is lost
1520
before it is responded to).
1523
def _massageError(error):
1524
error.trap(RemoteAmpError)
1526
errorType = self.reverseErrors.get(rje.errorCode,
1528
return Failure(errorType(rje.description))
1530
d = proto._sendBoxCommand(self.commandName,
1531
self.makeArguments(self.structured, proto),
1532
self.requiresAnswer)
1534
if self.requiresAnswer:
1535
d.addCallback(self.parseResponse, proto)
1536
d.addErrback(_massageError)
1542
class _NoCertificate:
1544
This is for peers which don't want to use a local certificate. Used by
1545
AMP because AMP's internal language is all about certificates and this
1546
duck-types in the appropriate place; this API isn't really stable though,
1547
so it's not exposed anywhere public.
1549
For clients, it will use ephemeral DH keys, or whatever the default is for
1550
certificate-less clients in OpenSSL. For servers, it will generate a
1551
temporary self-signed certificate with garbage values in the DN and use
1555
def __init__(self, client):
1557
Create a _NoCertificate which either is or isn't for the client side of
1560
@param client: True if we are a client and should truly have no
1561
certificate and be anonymous, False if we are a server and actually
1562
have to generate a temporary certificate.
1566
self.client = client
1569
def options(self, *authorities):
1571
Behaves like L{twisted.internet.ssl.PrivateCertificate.options}().
1574
# do some crud with sslverify to generate a temporary self-signed
1575
# certificate. This is SLOOOWWWWW so it is only in the absolute
1576
# worst, most naive case.
1578
# We have to do this because OpenSSL will not let both the server
1579
# and client be anonymous.
1580
sharedDN = DN(CN='TEMPORARY CERTIFICATE')
1581
key = KeyPair.generate()
1582
cr = key.certificateRequest(sharedDN)
1583
sscrd = key.signCertificateRequest(sharedDN, cr, lambda dn: True, 1)
1584
cert = key.newCertificate(sscrd)
1585
return cert.options(*authorities)
1588
options.update(dict(verify=True,
1589
requireCertificate=True,
1590
caCerts=[auth.original for auth in authorities]))
1591
occo = CertificateOptions(**options)
1596
class _TLSBox(AmpBox):
1598
I am an AmpBox that, upon being sent, initiates a TLS connection.
1602
def _keyprop(k, default):
1603
return property(lambda self: self.get(k, default))
1606
# These properties are described in startTLS
1607
certificate = _keyprop('tls_localCertificate', _NoCertificate(False))
1608
verify = _keyprop('tls_verifyAuthorities', None)
1610
def _sendTo(self, proto):
1612
Send my encoded value to the protocol, then initiate TLS.
1615
for k in ['tls_localCertificate',
1616
'tls_verifyAuthorities']:
1619
proto._startTLS(self.certificate, self.verify)
1623
class _LocalArgument(String):
1625
Local arguments are never actually relayed across the wire. This is just a
1626
shim so that StartTLS can pretend to have some arguments: if arguments
1627
acquire documentation properties, replace this with something nicer later.
1630
def fromBox(self, name, strings, objects, proto):
1635
class StartTLS(Command):
1637
Use, or subclass, me to implement a command that starts TLS.
1639
Callers of StartTLS may pass several special arguments, which affect the
1642
- tls_localCertificate: This is a
1643
twisted.internet.ssl.PrivateCertificate which will be used to secure
1644
the side of the connection it is returned on.
1646
- tls_verifyAuthorities: This is a list of
1647
twisted.internet.ssl.Certificate objects that will be used as the
1648
certificate authorities to verify our peer's certificate.
1650
Each of those special parameters may also be present as a key in the
1651
response dictionary.
1654
arguments = [("tls_localCertificate", _LocalArgument(optional=True)),
1655
("tls_verifyAuthorities", _LocalArgument(optional=True))]
1657
response = [("tls_localCertificate", _LocalArgument(optional=True)),
1658
("tls_verifyAuthorities", _LocalArgument(optional=True))]
1660
responseType = _TLSBox
1662
def __init__(self, **kw):
1664
Create a StartTLS command. (This is private. Use AMP.callRemote.)
1666
@param tls_localCertificate: the PrivateCertificate object to use to
1667
secure the connection. If it's None, or unspecified, an ephemeral DH
1668
key is used instead.
1670
@param tls_verifyAuthorities: a list of Certificate objects which
1671
represent root certificates to verify our peer with.
1673
self.certificate = kw.pop('tls_localCertificate', _NoCertificate(True))
1674
self.authorities = kw.pop('tls_verifyAuthorities', None)
1675
Command.__init__(self, **kw)
1678
def _doCommand(self, proto):
1680
When a StartTLS command is sent, prepare to start TLS, but don't actually
1681
do it; wait for the acknowledgement, then initiate the TLS handshake.
1683
d = Command._doCommand(self, proto)
1684
proto._prepareTLS(self.certificate, self.authorities)
1685
# XXX before we get back to user code we are going to start TLS...
1686
def actuallystart(response):
1687
proto._startTLS(self.certificate, self.authorities)
1689
d.addCallback(actuallystart)
1694
class ProtocolSwitchCommand(Command):
1696
Use this command to switch from something Amp-derived to a different
1697
protocol mid-connection. This can be useful to use amp as the
1698
connection-startup negotiation phase. Since TLS is a different layer
1699
entirely, you can use Amp to negotiate the security parameters of your
1700
connection, then switch to a different protocol, and the connection will
1704
def __init__(self, _protoToSwitchToFactory, **kw):
1706
Create a ProtocolSwitchCommand.
1708
@param _protoToSwitchToFactory: a ProtocolFactory which will generate
1709
the Protocol to switch to.
1711
@param kw: Keyword arguments, encoded and handled normally as
1715
self.protoToSwitchToFactory = _protoToSwitchToFactory
1716
super(ProtocolSwitchCommand, self).__init__(**kw)
1719
def makeResponse(cls, innerProto, proto):
1720
return _SwitchBox(innerProto)
1721
makeResponse = classmethod(makeResponse)
1724
def _doCommand(self, proto):
1726
When we emit a ProtocolSwitchCommand, lock the protocol, but don't actually
1727
switch to the new protocol unless an acknowledgement is received. If
1728
an error is received, switch back.
1730
d = super(ProtocolSwitchCommand, self)._doCommand(proto)
1731
proto._lockForSwitch()
1733
innerProto = self.protoToSwitchToFactory.buildProtocol(
1734
proto.transport.getPeer())
1735
proto._switchTo(innerProto, self.protoToSwitchToFactory)
1738
proto._unlockFromSwitch()
1739
self.protoToSwitchToFactory.clientConnectionFailed(
1740
None, Failure(CONNECTION_LOST))
1742
return d.addCallbacks(switchNow, handle)
1746
class BinaryBoxProtocol(StatefulStringProtocol, Int16StringReceiver):
1748
A protocol for receving L{Box}es - key/value pairs - via length-prefixed
1749
strings. A box is composed of:
1751
- any number of key-value pairs, described by:
1752
- a 2-byte network-endian packed key length (of which the first
1753
byte must be null, and the second must be non-null: i.e. the
1754
value of the length must be 1-255)
1755
- a key, comprised of that many bytes
1756
- a 2-byte network-endian unsigned value length (up to the maximum
1758
- a value, comprised of that many bytes
1761
In other words, an even number of strings prefixed with packed unsigned
1762
16-bit integers, and then a 0-length string to indicate the end of the box.
1764
This protocol also implements 2 extra private bits of functionality related
1765
to the byte boundaries between messages; it can start TLS between two given
1766
boxes or switch to an entirely different protocol. However, due to some
1767
tricky elements of the implementation, the public interface to this
1768
functionality is L{ProtocolSwitchCommand} and L{StartTLS}.
1770
@ivar boxReceiver: an L{IBoxReceiver} provider, whose L{ampBoxReceived}
1771
method will be invoked for each L{Box} that is received.
1774
implements(IBoxSender)
1776
_justStartedTLS = False
1777
_startingTLSBuffer = None
1782
hostCertificate = None
1783
noPeerCertificate = False # for tests
1784
innerProtocol = None
1785
innerProtocolClientFactory = None
1787
_sslVerifyProblems = ()
1788
# ^ Later this will become a mutable list - we can't get the handle during
1789
# connection shutdown thanks to the fact that Twisted destroys the socket
1790
# on our transport before notifying us of a lost connection (which I guess
1791
# is reasonable - the socket is dead by then) See a few lines below in
1792
# startTLS for details. --glyph
1795
def __init__(self, boxReceiver):
1796
self.boxReceiver = boxReceiver
1799
def _switchTo(self, newProto, clientFactory=None):
1801
Switch this BinaryBoxProtocol's transport to a new protocol. You need
1802
to do this 'simultaneously' on both ends of a connection; the easiest
1803
way to do this is to use a subclass of ProtocolSwitchCommand.
1805
@param newProto: the new protocol instance to switch to.
1807
@param clientFactory: the ClientFactory to send the
1808
L{clientConnectionLost} notification to.
1810
# All the data that Int16Receiver has not yet dealt with belongs to our
1811
# new protocol: luckily it's keeping that in a handy (although
1812
# ostensibly internal) variable for us:
1813
newProtoData = self.recvd
1814
# We're quite possibly in the middle of a 'dataReceived' loop in
1815
# Int16StringReceiver: let's make sure that the next iteration, the
1816
# loop will break and not attempt to look at something that isn't a
1819
# Finally, do the actual work of setting up the protocol and delivering
1820
# its first chunk of data, if one is available.
1821
self.innerProtocol = newProto
1822
self.innerProtocolClientFactory = clientFactory
1823
newProto.makeConnection(self.transport)
1824
newProto.dataReceived(newProtoData)
1827
def sendBox(self, box):
1829
Send a amp.Box to my peer.
1831
Note: transport.write is never called outside of this method.
1833
@param box: an AmpBox.
1835
@raise ProtocolSwitched: if the protocol has previously been switched.
1837
@raise ConnectionLost: if the connection has previously been lost.
1840
raise ProtocolSwitched(
1841
"This connection has switched: no AMP traffic allowed.")
1842
if self.transport is None:
1843
raise ConnectionLost()
1844
if self._startingTLSBuffer is not None:
1845
self._startingTLSBuffer.append(box)
1847
self.transport.write(box.serialize())
1850
def makeConnection(self, transport):
1852
Notify L{boxReceiver} that it is about to receive boxes from this
1853
protocol by invoking L{startReceivingBoxes}.
1855
self.boxReceiver.startReceivingBoxes(self)
1856
Int16StringReceiver.makeConnection(self, transport)
1859
def dataReceived(self, data):
1861
Either parse incoming data as L{AmpBox}es or relay it to our nested
1864
if self._justStartedTLS:
1865
self._justStartedTLS = False
1866
# If we already have an inner protocol, then we don't deliver data to
1867
# the protocol parser any more; we just hand it off.
1868
if self.innerProtocol is not None:
1869
self.innerProtocol.dataReceived(data)
1871
return Int16StringReceiver.dataReceived(self, data)
1874
def connectionLost(self, reason):
1876
The connection was lost; notify any nested protocol.
1878
if self.innerProtocol is not None:
1879
self.innerProtocol.connectionLost(reason)
1880
if self.innerProtocolClientFactory is not None:
1881
self.innerProtocolClientFactory.clientConnectionLost(None, reason)
1882
# XXX this may be a slight oversimplification, but I believe that if
1883
# there are pending SSL errors, they _are_ the reason that the
1884
# connection was lost. a totally correct implementation of this would
1885
# set up a simple state machine to track whether any bytes were
1886
# received after startTLS was called. --glyph
1887
problems = self._sslVerifyProblems
1889
failReason = Failure(problems[0])
1890
elif self._justStartedTLS:
1891
# We just started TLS and haven't received any data. This means
1892
# the other connection didn't like our cert (although they may not
1893
# have told us why - later Twisted should make 'reason' into a TLS
1895
failReason = PeerVerifyError(
1896
"Peer rejected our certificate for an unknown reason.")
1899
self.boxReceiver.stopReceivingBoxes(failReason)
1903
def proto_init(self, string):
1905
String received in the 'init' state.
1907
self._currentBox = AmpBox()
1908
return self.proto_key(string)
1911
def proto_key(self, string):
1913
String received in the 'key' state. If the key is empty, a complete
1914
box has been received.
1917
self._currentKey = string
1920
self.boxReceiver.ampBoxReceived(self._currentBox)
1921
self._currentBox = None
1925
def proto_value(self, string):
1927
String received in the 'value' state.
1929
self._currentBox[self._currentKey] = string
1930
self._currentKey = None
1934
def _lockForSwitch(self):
1936
Lock this binary protocol so that no further boxes may be sent. This
1937
is used when sending a request to switch underlying protocols. You
1938
probably want to subclass ProtocolSwitchCommand rather than calling
1944
def _unlockFromSwitch(self):
1946
Unlock this locked binary protocol so that further boxes may be sent
1947
again. This is used after an attempt to switch protocols has failed
1950
if self.innerProtocol is not None:
1951
raise ProtocolSwitched("Protocol already switched. Cannot unlock.")
1952
self._locked = False
1955
def _prepareTLS(self, certificate, verifyAuthorities):
1957
Used by StartTLSCommand to put us into the state where we don't
1958
actually send things that get sent, instead we buffer them. see
1961
self._startingTLSBuffer = []
1962
if self.hostCertificate is not None:
1964
"Previously authenticated connection between %s and %s "
1965
"is trying to re-establish as %s" % (
1966
self.hostCertificate,
1967
self.peerCertificate,
1968
(certificate, verifyAuthorities)))
1971
def _startTLS(self, certificate, verifyAuthorities):
1973
Used by TLSBox to initiate the SSL handshake.
1975
@param certificate: a L{twisted.internet.ssl.PrivateCertificate} for
1978
@param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances
1979
representing certificate authorities which will verify our peer.
1981
self.hostCertificate = certificate
1982
self._justStartedTLS = True
1983
if verifyAuthorities is None:
1984
verifyAuthorities = ()
1985
self.transport.startTLS(certificate.options(*verifyAuthorities))
1986
# Remember that mutable list that we were just talking about? Here
1987
# it is. sslverify.py takes care of populating this list as
1988
# necessary. --glyph
1989
self._sslVerifyProblems = problemsFromTransport(self.transport)
1990
stlsb = self._startingTLSBuffer
1991
if stlsb is not None:
1992
self._startingTLSBuffer = None
1997
def _getPeerCertificate(self):
1998
if self.noPeerCertificate:
2000
return Certificate.peerFromTransport(self.transport)
2001
peerCertificate = property(_getPeerCertificate)
2004
def unhandledError(self, failure):
2006
The buck stops here. This error was completely unhandled, time to
2007
terminate the connection.
2009
log.msg("Amp server or network failure "
2010
"unhandled by client application:")
2013
"Dropping connection! "
2014
"To avoid, add errbacks to ALL remote commands!")
2015
if self.transport is not None:
2016
self.transport.loseConnection()
2019
def _defaultStartTLSResponder(self):
2021
The default TLS responder doesn't specify any certificate or anything.
2023
From a security perspective, it's little better than a plain-text
2024
connection - but it is still a *bit* better, so it's included for
2027
You probably want to override this by providing your own StartTLS.responder.
2030
StartTLS.responder(_defaultStartTLSResponder)
2034
class AMP(BinaryBoxProtocol, BoxDispatcher,
2035
CommandLocator, SimpleStringLocator):
2037
This protocol is an AMP connection. See the module docstring for protocol
2041
_ampInitialized = False
2043
def __init__(self, boxReceiver=None, locator=None):
2044
# For backwards compatibility. When AMP did not separate parsing logic
2045
# (L{BinaryBoxProtocol}), request-response logic (L{BoxDispatcher}) and
2046
# command routing (L{CommandLocator}), it did not have a constructor.
2047
# Now it does, so old subclasses might have defined their own that did
2048
# not upcall. If this flag isn't set, we'll call the constructor in
2049
# makeConnection before anything actually happens.
2050
self._ampInitialized = True
2051
if boxReceiver is None:
2056
BoxDispatcher.__init__(self, locator)
2057
BinaryBoxProtocol.__init__(self, boxReceiver)
2060
def locateResponder(self, name):
2062
Unify the implementations of L{CommandLocator} and
2063
L{SimpleStringLocator} to perform both kinds of dispatch, preferring
2066
firstResponder = CommandLocator.locateResponder(self, name)
2067
if firstResponder is not None:
2068
return firstResponder
2069
secondResponder = SimpleStringLocator.locateResponder(self, name)
2070
return secondResponder
2075
A verbose string representation which gives us information about this
2078
return '<%s %s at 0x%x>' % (
2079
self.__class__.__name__,
2080
self.innerProtocol, id(self))
2083
def makeConnection(self, transport):
2085
Emit a helpful log message when the connection is made.
2087
if not self._ampInitialized:
2088
# See comment in the constructor re: backward compatibility. I
2089
# should probably emit a deprecation warning here.
2091
# Save these so we can emit a similar log message in L{connectionLost}.
2092
self._transportPeer = transport.getPeer()
2093
self._transportHost = transport.getHost()
2094
log.msg("%s connection established (HOST:%s PEER:%s)" % (
2095
self.__class__.__name__,
2096
self._transportHost,
2097
self._transportPeer))
2098
BinaryBoxProtocol.makeConnection(self, transport)
2101
def connectionLost(self, reason):
2103
Emit a helpful log message when the connection is lost.
2105
log.msg("%s connection lost (HOST:%s PEER:%s)" %
2106
(self.__class__.__name__,
2107
self._transportHost,
2108
self._transportPeer))
2109
BinaryBoxProtocol.connectionLost(self, reason)
2110
self.transport = None
2114
class _ParserHelper:
2116
A box receiver which records all boxes received.
2129
disconnecting = False
2132
def startReceivingBoxes(self, sender):
2134
No initialization is required.
2138
def ampBoxReceived(self, box):
2139
self.boxes.append(box)
2142
# Synchronous helpers
2143
def parse(cls, fileObj):
2145
Parse some amp data stored in a file.
2147
@param fileObj: a file-like object.
2149
@return: a list of AmpBoxes encoded in the given file.
2151
parserHelper = cls()
2152
bbp = BinaryBoxProtocol(boxReceiver=parserHelper)
2153
bbp.makeConnection(parserHelper)
2154
bbp.dataReceived(fileObj.read())
2155
return parserHelper.boxes
2156
parse = classmethod(parse)
2159
def parseString(cls, data):
2161
Parse some amp data stored in a string.
2163
@param data: a str holding some amp-encoded data.
2165
@return: a list of AmpBoxes encoded in the given string.
2167
return cls.parse(StringIO(data))
2168
parseString = classmethod(parseString)
2172
parse = _ParserHelper.parse
2173
parseString = _ParserHelper.parseString
2175
def _stringsToObjects(strings, arglist, proto):
2177
Convert an AmpBox to a dictionary of python objects, converting through a
2180
@param strings: an AmpBox (or dict of strings)
2182
@param arglist: a list of 2-tuples of strings and Argument objects, as
2183
described in L{Command.arguments}.
2185
@param proto: an L{AMP} instance.
2187
@return: the converted dictionary mapping names to argument objects.
2190
myStrings = strings.copy()
2191
for argname, argparser in arglist:
2192
argparser.fromBox(argname, myStrings, objects, proto)
2197
def _objectsToStrings(objects, arglist, strings, proto):
2199
Convert a dictionary of python objects to an AmpBox, converting through a
2202
@param objects: a dict mapping names to python objects
2204
@param arglist: a list of 2-tuples of strings and Argument objects, as
2205
described in L{Command.arguments}.
2207
@param strings: [OUT PARAMETER] An object providing the L{dict}
2208
interface which will be populated with serialized data.
2210
@param proto: an L{AMP} instance.
2212
@return: The converted dictionary mapping names to encoded argument
2213
strings (identical to C{strings}).
2216
for (k, v) in objects.items():
2219
for argname, argparser in arglist:
2220
argparser.toBox(argname, strings, myObjects, proto)