| OLD | NEW |
| (Empty) |
| 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. | |
| 5 | |
| 6 """ | |
| 7 This module implements AMP, the Asynchronous Messaging Protocol. | |
| 8 | |
| 9 AMP is a protocol for sending multiple asynchronous request/response pairs over | |
| 10 the same connection. Requests and responses are both collections of key/value | |
| 11 pairs. | |
| 12 | |
| 13 AMP is a very simple protocol which is not an application. This module is a | |
| 14 "protocol construction kit" of sorts; it attempts to be the simplest wire-level | |
| 15 implementation of Deferreds. AMP provides the following base-level features: | |
| 16 | |
| 17 - Asynchronous request/response handling (hence the name) | |
| 18 | |
| 19 - Requests and responses are both key/value pairs | |
| 20 | |
| 21 - Binary transfer of all data: all data is length-prefixed. Your | |
| 22 application will never need to worry about quoting. | |
| 23 | |
| 24 - Command dispatching (like HTTP Verbs): the protocol is extensible, and | |
| 25 multiple AMP sub-protocols can be grouped together easily. | |
| 26 | |
| 27 The protocol implementation also provides a few additional features which are | |
| 28 not part of the core wire protocol, but are nevertheless very useful: | |
| 29 | |
| 30 - Tight TLS integration, with an included StartTLS command. | |
| 31 | |
| 32 - Handshaking to other protocols: because AMP has well-defined message | |
| 33 boundaries and maintains all incoming and outgoing requests for you, you | |
| 34 can start a connection over AMP and then switch to another protocol. | |
| 35 This makes it ideal for firewall-traversal applications where you may | |
| 36 have only one forwarded port but multiple applications that want to use | |
| 37 it. | |
| 38 | |
| 39 Using AMP with Twisted is simple. Each message is a command, with a response. | |
| 40 You begin by defining a command type. Commands specify their input and output | |
| 41 in terms of the types that they expect to see in the request and response | |
| 42 key-value pairs. Here's an example of a command that adds two integers, 'a' | |
| 43 and 'b':: | |
| 44 | |
| 45 class Sum(amp.Command): | |
| 46 arguments = [('a', amp.Integer()), | |
| 47 ('b', amp.Integer())] | |
| 48 response = [('total', amp.Integer())] | |
| 49 | |
| 50 Once you have specified a command, you need to make it part of a protocol, and | |
| 51 define a responder for it. Here's a 'JustSum' protocol that includes a | |
| 52 responder for our 'Sum' command:: | |
| 53 | |
| 54 class JustSum(amp.AMP): | |
| 55 def sum(self, a, b): | |
| 56 total = a + b | |
| 57 print 'Did a sum: %d + %d = %d' % (a, b, total) | |
| 58 return {'total': total} | |
| 59 Sum.responder(sum) | |
| 60 | |
| 61 Later, when you want to actually do a sum, the following expression will return | |
| 62 a Deferred which will fire with the result:: | |
| 63 | |
| 64 ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback( | |
| 65 lambda p: p.callRemote(Sum, a=13, b=81)).addCallback( | |
| 66 lambda result: result['total']) | |
| 67 | |
| 68 You can also define the propagation of specific errors in AMP. For example, | |
| 69 for the slightly more complicated case of division, we might have to deal with | |
| 70 division by zero:: | |
| 71 | |
| 72 class Divide(amp.Command): | |
| 73 arguments = [('numerator', amp.Integer()), | |
| 74 ('denominator', amp.Integer())] | |
| 75 response = [('result', amp.Float())] | |
| 76 errors = {ZeroDivisionError: 'ZERO_DIVISION'} | |
| 77 | |
| 78 The 'errors' mapping here tells AMP that if a responder to Divide emits a | |
| 79 L{ZeroDivisionError}, then the other side should be informed that an error of | |
| 80 the type 'ZERO_DIVISION' has occurred. Writing a responder which takes | |
| 81 advantage of this is very simple - just raise your exception normally:: | |
| 82 | |
| 83 class JustDivide(amp.AMP): | |
| 84 def divide(self, numerator, denominator): | |
| 85 result = numerator / denominator | |
| 86 print 'Divided: %d / %d = %d' % (numerator, denominator, total) | |
| 87 return {'result': result} | |
| 88 Divide.responder(divide) | |
| 89 | |
| 90 On the client side, the errors mapping will be used to determine what the | |
| 91 'ZERO_DIVISION' error means, and translated into an asynchronous exception, | |
| 92 which can be handled normally as any L{Deferred} would be:: | |
| 93 | |
| 94 def trapZero(result): | |
| 95 result.trap(ZeroDivisionError) | |
| 96 print "Divided by zero: returning INF" | |
| 97 return 1e1000 | |
| 98 ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback( | |
| 99 lambda p: p.callRemote(Divide, numerator=1234, | |
| 100 denominator=0) | |
| 101 ).addErrback(trapZero) | |
| 102 | |
| 103 For a complete, runnable example of both of these commands, see the files in | |
| 104 the Twisted repository:: | |
| 105 | |
| 106 doc/core/examples/ampserver.py | |
| 107 doc/core/examples/ampclient.py | |
| 108 | |
| 109 On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and | |
| 110 values, and empty keys to separate messages:: | |
| 111 | |
| 112 <2-byte length><key><2-byte length><value> | |
| 113 <2-byte length><key><2-byte length><value> | |
| 114 ... | |
| 115 <2-byte length><key><2-byte length><value> | |
| 116 <NUL><NUL> # Empty Key == End of Message | |
| 117 | |
| 118 And so on. Because it's tedious to refer to lengths and NULs constantly, the | |
| 119 documentation will refer to packets as if they were newline delimited, like | |
| 120 so:: | |
| 121 | |
| 122 C: _command: sum | |
| 123 C: _ask: ef639e5c892ccb54 | |
| 124 C: a: 13 | |
| 125 C: b: 81 | |
| 126 | |
| 127 S: _answer: ef639e5c892ccb54 | |
| 128 S: total: 94 | |
| 129 | |
| 130 Notes: | |
| 131 | |
| 132 Values are limited to the maximum encodable size in a 16-bit length, 65535 | |
| 133 bytes. | |
| 134 | |
| 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: | |
| 138 | |
| 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. | |
| 142 | |
| 143 - A single NUL will separate every key, and a double NUL separates | |
| 144 messages. This provides some redundancy when debugging traffic dumps. | |
| 145 | |
| 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. | |
| 149 | |
| 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. | |
| 154 | |
| 155 """ | |
| 156 | |
| 157 __metaclass__ = type | |
| 158 | |
| 159 import types, warnings | |
| 160 | |
| 161 from cStringIO import StringIO | |
| 162 from struct import pack | |
| 163 | |
| 164 from zope.interface import Interface, implements | |
| 165 | |
| 166 from twisted.python.reflect import accumulateClassDict | |
| 167 from twisted.python.failure import Failure | |
| 168 from twisted.python import log, filepath | |
| 169 | |
| 170 from twisted.internet.main import CONNECTION_LOST | |
| 171 from twisted.internet.error import PeerVerifyError, ConnectionLost | |
| 172 from twisted.internet.defer import Deferred, maybeDeferred, fail | |
| 173 from twisted.protocols.basic import Int16StringReceiver, StatefulStringProtocol | |
| 174 | |
| 175 from twisted.internet._sslverify import problemsFromTransport | |
| 176 | |
| 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. | |
| 179 | |
| 180 from twisted.internet.ssl import CertificateOptions, Certificate, DN, KeyPair | |
| 181 | |
| 182 ASK = '_ask' | |
| 183 ANSWER = '_answer' | |
| 184 COMMAND = '_command' | |
| 185 ERROR = '_error' | |
| 186 ERROR_CODE = '_error_code' | |
| 187 ERROR_DESCRIPTION = '_error_description' | |
| 188 UNKNOWN_ERROR_CODE = 'UNKNOWN' | |
| 189 UNHANDLED_ERROR_CODE = 'UNHANDLED' | |
| 190 | |
| 191 MAX_KEY_LENGTH = 0xff | |
| 192 MAX_VALUE_LENGTH = 0xffff | |
| 193 | |
| 194 | |
| 195 class IBoxSender(Interface): | |
| 196 """ | |
| 197 A transport which can send L{AmpBox} objects. | |
| 198 """ | |
| 199 | |
| 200 def sendBox(box): | |
| 201 """ | |
| 202 Send an L{AmpBox}. | |
| 203 | |
| 204 @raise ProtocolSwitched: if the underlying protocol has been | |
| 205 switched. | |
| 206 | |
| 207 @raise ConnectionLost: if the underlying connection has already been | |
| 208 lost. | |
| 209 """ | |
| 210 | |
| 211 def unhandledError(failure): | |
| 212 """ | |
| 213 An unhandled error occurred in response to a box. Log it | |
| 214 appropriately. | |
| 215 | |
| 216 @param failure: a L{Failure} describing the error that occurred. | |
| 217 """ | |
| 218 | |
| 219 | |
| 220 | |
| 221 class IBoxReceiver(Interface): | |
| 222 """ | |
| 223 An application object which can receive L{AmpBox} objects and dispatch them | |
| 224 appropriately. | |
| 225 """ | |
| 226 | |
| 227 def startReceivingBoxes(boxSender): | |
| 228 """ | |
| 229 The L{ampBoxReceived} method will start being called; boxes may be | |
| 230 responded to by responding to the given L{IBoxSender}. | |
| 231 | |
| 232 @param boxSender: an L{IBoxSender} provider. | |
| 233 """ | |
| 234 | |
| 235 | |
| 236 def ampBoxReceived(box): | |
| 237 """ | |
| 238 A box was received from the transport; dispatch it appropriately. | |
| 239 """ | |
| 240 | |
| 241 | |
| 242 def stopReceivingBoxes(reason): | |
| 243 """ | |
| 244 No further boxes will be received on this connection. | |
| 245 | |
| 246 @type reason: L{Failure} | |
| 247 """ | |
| 248 | |
| 249 | |
| 250 | |
| 251 class IResponderLocator(Interface): | |
| 252 """ | |
| 253 An application object which can look up appropriate responder methods for | |
| 254 AMP commands. | |
| 255 """ | |
| 256 | |
| 257 def locateResponder(self, name): | |
| 258 """ | |
| 259 Locate a responder method appropriate for the named command. | |
| 260 | |
| 261 @param name: the wire-level name (commandName) of the AMP command to be | |
| 262 responded to. | |
| 263 | |
| 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 | |
| 267 same. | |
| 268 """ | |
| 269 | |
| 270 | |
| 271 | |
| 272 class AmpError(Exception): | |
| 273 """ | |
| 274 Base class of all Amp-related exceptions. | |
| 275 """ | |
| 276 | |
| 277 | |
| 278 | |
| 279 class ProtocolSwitched(Exception): | |
| 280 """ | |
| 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. | |
| 283 """ | |
| 284 | |
| 285 | |
| 286 | |
| 287 class OnlyOneTLS(AmpError): | |
| 288 """ | |
| 289 This is an implementation limitation; TLS may only be started once per | |
| 290 connection. | |
| 291 """ | |
| 292 | |
| 293 | |
| 294 | |
| 295 class NoEmptyBoxes(AmpError): | |
| 296 """ | |
| 297 You can't have empty boxes on the connection. This is raised when you | |
| 298 receive or attempt to send one. | |
| 299 """ | |
| 300 | |
| 301 | |
| 302 | |
| 303 class InvalidSignature(AmpError): | |
| 304 """ | |
| 305 You didn't pass all the required arguments. | |
| 306 """ | |
| 307 | |
| 308 | |
| 309 | |
| 310 class TooLong(AmpError): | |
| 311 """ | |
| 312 One of the protocol's length limitations was violated. | |
| 313 | |
| 314 @ivar isKey: true if the string being encoded in a key position, false if | |
| 315 it was in a value position. | |
| 316 | |
| 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.) | |
| 320 | |
| 321 @ivar value: The string that was too long. | |
| 322 | |
| 323 @ivar keyName: If the string being encoded was in a value position, what | |
| 324 key was it being encoded for? | |
| 325 """ | |
| 326 | |
| 327 def __init__(self, isKey, isLocal, value, keyName=None): | |
| 328 AmpError.__init__(self) | |
| 329 self.isKey = isKey | |
| 330 self.isLocal = isLocal | |
| 331 self.value = value | |
| 332 self.keyName = keyName | |
| 333 | |
| 334 | |
| 335 def __repr__(self): | |
| 336 hdr = self.isKey and "key" or "value" | |
| 337 if not self.isKey: | |
| 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)) | |
| 341 | |
| 342 | |
| 343 | |
| 344 class BadLocalReturn(AmpError): | |
| 345 """ | |
| 346 A bad value was returned from a local command; we were unable to coerce it. | |
| 347 """ | |
| 348 def __init__(self, message, enclosed): | |
| 349 AmpError.__init__(self) | |
| 350 self.message = message | |
| 351 self.enclosed = enclosed | |
| 352 | |
| 353 | |
| 354 def __repr__(self): | |
| 355 return self.message + " " + self.enclosed.getBriefTraceback() | |
| 356 | |
| 357 __str__ = __repr__ | |
| 358 | |
| 359 | |
| 360 | |
| 361 class RemoteAmpError(AmpError): | |
| 362 """ | |
| 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. | |
| 365 """ | |
| 366 def __init__(self, errorCode, description, fatal=False, local=None): | |
| 367 """Create a remote error with an error code and description. | |
| 368 | |
| 369 @param errorCode: the AMP error code of this error. | |
| 370 | |
| 371 @param description: some text to show to the user. | |
| 372 | |
| 373 @param fatal: a boolean, true if this error should terminate the | |
| 374 connection. | |
| 375 | |
| 376 @param local: a local Failure, if one exists. | |
| 377 """ | |
| 378 if local: | |
| 379 localwhat = ' (local)' | |
| 380 othertb = local.getBriefTraceback() | |
| 381 else: | |
| 382 localwhat = '' | |
| 383 othertb = '' | |
| 384 Exception.__init__(self, "Code<%s>%s: %s%s" % ( | |
| 385 errorCode, localwhat, | |
| 386 description, othertb)) | |
| 387 self.local = local | |
| 388 self.errorCode = errorCode | |
| 389 self.description = description | |
| 390 self.fatal = fatal | |
| 391 | |
| 392 | |
| 393 | |
| 394 class UnknownRemoteError(RemoteAmpError): | |
| 395 """ | |
| 396 This means that an error whose type we can't identify was raised from the | |
| 397 other side. | |
| 398 """ | |
| 399 def __init__(self, description): | |
| 400 errorCode = UNKNOWN_ERROR_CODE | |
| 401 RemoteAmpError.__init__(self, errorCode, description) | |
| 402 | |
| 403 | |
| 404 | |
| 405 class MalformedAmpBox(AmpError): | |
| 406 """ | |
| 407 This error indicates that the wire-level protocol was malformed. | |
| 408 """ | |
| 409 | |
| 410 | |
| 411 | |
| 412 class UnhandledCommand(AmpError): | |
| 413 """ | |
| 414 A command received via amp could not be dispatched. | |
| 415 """ | |
| 416 | |
| 417 | |
| 418 | |
| 419 class IncompatibleVersions(AmpError): | |
| 420 """ | |
| 421 It was impossible to negotiate a compatible version of the protocol with | |
| 422 the other end of the connection. | |
| 423 """ | |
| 424 | |
| 425 | |
| 426 PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand} | |
| 427 | |
| 428 class AmpBox(dict): | |
| 429 """ | |
| 430 I am a packet in the AMP protocol, much like a regular str:str dictionary. | |
| 431 """ | |
| 432 __slots__ = [] # be like a regular dictionary, don't magically | |
| 433 # acquire a __dict__... | |
| 434 | |
| 435 | |
| 436 def copy(self): | |
| 437 """ | |
| 438 Return another AmpBox just like me. | |
| 439 """ | |
| 440 newBox = self.__class__() | |
| 441 newBox.update(self) | |
| 442 return newBox | |
| 443 | |
| 444 | |
| 445 def serialize(self): | |
| 446 """ | |
| 447 Convert me into a wire-encoded string. | |
| 448 | |
| 449 @return: a str encoded according to the rules described in the module | |
| 450 docstring. | |
| 451 """ | |
| 452 i = self.items() | |
| 453 i.sort() | |
| 454 L = [] | |
| 455 w = L.append | |
| 456 for k, v in i: | |
| 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) | |
| 461 for kv in k, v: | |
| 462 w(pack("!H", len(kv))) | |
| 463 w(kv) | |
| 464 w(pack("!H", 0)) | |
| 465 return ''.join(L) | |
| 466 | |
| 467 | |
| 468 def _sendTo(self, proto): | |
| 469 """ | |
| 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:: | |
| 472 | |
| 473 _ask | |
| 474 _answer | |
| 475 _error | |
| 476 | |
| 477 If the '_ask' key is set, then the '_command' key must also be | |
| 478 set. | |
| 479 | |
| 480 @param proto: an AMP instance. | |
| 481 """ | |
| 482 proto.sendBox(self) | |
| 483 | |
| 484 def __repr__(self): | |
| 485 return 'AmpBox(%s)' % (dict.__repr__(self),) | |
| 486 | |
| 487 # amp.Box => AmpBox | |
| 488 | |
| 489 Box = AmpBox | |
| 490 | |
| 491 class QuitBox(AmpBox): | |
| 492 """ | |
| 493 I am an AmpBox that, upon being sent, terminates the connection. | |
| 494 """ | |
| 495 __slots__ = [] | |
| 496 | |
| 497 | |
| 498 def __repr__(self): | |
| 499 return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),) | |
| 500 | |
| 501 | |
| 502 def _sendTo(self, proto): | |
| 503 """ | |
| 504 Immediately call loseConnection after sending. | |
| 505 """ | |
| 506 super(QuitBox, self)._sendTo(proto) | |
| 507 proto.transport.loseConnection() | |
| 508 | |
| 509 | |
| 510 | |
| 511 class _SwitchBox(AmpBox): | |
| 512 """ | |
| 513 Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets | |
| 514 up state for the protocol to switch. | |
| 515 """ | |
| 516 | |
| 517 # DON'T set __slots__ here; we do have an attribute. | |
| 518 | |
| 519 def __init__(self, innerProto, **kw): | |
| 520 """ | |
| 521 Create a _SwitchBox with the protocol to switch to after being sent. | |
| 522 | |
| 523 @param innerProto: the protocol instance to switch to. | |
| 524 @type innerProto: an IProtocol provider. | |
| 525 """ | |
| 526 super(_SwitchBox, self).__init__(**kw) | |
| 527 self.innerProto = innerProto | |
| 528 | |
| 529 | |
| 530 def __repr__(self): | |
| 531 return '_SwitchBox(%r, **%s)' % (self.innerProto, | |
| 532 dict.__repr__(self),) | |
| 533 | |
| 534 | |
| 535 def _sendTo(self, proto): | |
| 536 """ | |
| 537 Send me; I am the last box on the connection. All further traffic will
be | |
| 538 over the new protocol. | |
| 539 """ | |
| 540 super(_SwitchBox, self)._sendTo(proto) | |
| 541 proto._lockForSwitch() | |
| 542 proto._switchTo(self.innerProto) | |
| 543 | |
| 544 | |
| 545 | |
| 546 class BoxDispatcher: | |
| 547 """ | |
| 548 A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es, | |
| 549 both incoming and outgoing, to their appropriate destinations. | |
| 550 | |
| 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. | |
| 555 | |
| 556 Incoming '_ask' boxes are converted into method calls on a supplied method | |
| 557 locator. | |
| 558 | |
| 559 @ivar _outstandingRequests: a dictionary mapping request IDs to | |
| 560 L{Deferred}s which were returned for those requests. | |
| 561 | |
| 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). | |
| 565 | |
| 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} | |
| 569 """ | |
| 570 | |
| 571 implements(IBoxReceiver) | |
| 572 | |
| 573 _failAllReason = None | |
| 574 _outstandingRequests = None | |
| 575 _counter = 0L | |
| 576 boxSender = None | |
| 577 | |
| 578 def __init__(self, locator): | |
| 579 self._outstandingRequests = {} | |
| 580 self.locator = locator | |
| 581 | |
| 582 | |
| 583 def startReceivingBoxes(self, boxSender): | |
| 584 """ | |
| 585 The given boxSender is going to start calling boxReceived on this | |
| 586 L{BoxDispatcher}. | |
| 587 | |
| 588 @param boxSender: The L{IBoxSender} to send command responses to. | |
| 589 """ | |
| 590 self.boxSender = boxSender | |
| 591 | |
| 592 | |
| 593 def stopReceivingBoxes(self, reason): | |
| 594 """ | |
| 595 No further boxes will be received here. Terminate all currently | |
| 596 oustanding command deferreds with the given reason. | |
| 597 """ | |
| 598 self.failAllOutgoing(reason) | |
| 599 | |
| 600 | |
| 601 def failAllOutgoing(self, reason): | |
| 602 """ | |
| 603 Call the errback on all outstanding requests awaiting responses. | |
| 604 | |
| 605 @param reason: the Failure instance to pass to those errbacks. | |
| 606 """ | |
| 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) | |
| 612 | |
| 613 | |
| 614 def _nextTag(self): | |
| 615 """ | |
| 616 Generate protocol-local serial numbers for _ask keys. | |
| 617 | |
| 618 @return: a string that has not yet been used on this connection. | |
| 619 """ | |
| 620 self._counter += 1 | |
| 621 return '%x' % (self._counter,) | |
| 622 | |
| 623 | |
| 624 def _sendBoxCommand(self, command, box, requiresAnswer=True): | |
| 625 """ | |
| 626 Send a command across the wire with the given C{amp.Box}. | |
| 627 | |
| 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. | |
| 630 | |
| 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. | |
| 634 | |
| 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. | |
| 637 | |
| 638 @param command: a str, the name of the command to issue. | |
| 639 | |
| 640 @param box: an AmpBox with the arguments for the command. | |
| 641 | |
| 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. | |
| 645 | |
| 646 @return: a Deferred which fires the AmpBox that holds the response to | |
| 647 this command, or None, as specified by requiresAnswer. | |
| 648 | |
| 649 @raise ProtocolSwitched: if the protocol has been switched. | |
| 650 """ | |
| 651 if self._failAllReason is not None: | |
| 652 return fail(self._failAllReason) | |
| 653 box[COMMAND] = command | |
| 654 tag = self._nextTag() | |
| 655 if requiresAnswer: | |
| 656 box[ASK] = tag | |
| 657 box._sendTo(self.boxSender) | |
| 658 if requiresAnswer: | |
| 659 result = self._outstandingRequests[tag] = Deferred() | |
| 660 else: | |
| 661 result = None | |
| 662 return result | |
| 663 | |
| 664 | |
| 665 def callRemoteString(self, command, requiresAnswer=True, **kw): | |
| 666 """ | |
| 667 This is a low-level API, designed only for optimizing simple messages | |
| 668 for which the overhead of parsing is too great. | |
| 669 | |
| 670 @param command: a str naming the command. | |
| 671 | |
| 672 @param kw: arguments to the amp box. | |
| 673 | |
| 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. | |
| 677 | |
| 678 @return: a Deferred which fires the AmpBox that holds the response to | |
| 679 this command, or None, as specified by requiresAnswer. | |
| 680 """ | |
| 681 box = Box(kw) | |
| 682 return self._sendBoxCommand(command, box) | |
| 683 | |
| 684 | |
| 685 def callRemote(self, commandType, *a, **kw): | |
| 686 """ | |
| 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 | |
| 689 connection's peer. | |
| 690 | |
| 691 @param commandType: a subclass of Command. | |
| 692 @type commandType: L{type} | |
| 693 | |
| 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. | |
| 699 | |
| 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}. | |
| 703 | |
| 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. | |
| 711 """ | |
| 712 | |
| 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. | |
| 718 | |
| 719 # In other words, the fact that commandType is instantiated here is an | |
| 720 # implementation detail. Don't rely on it. | |
| 721 | |
| 722 co = commandType(*a, **kw) | |
| 723 return co._doCommand(self) | |
| 724 | |
| 725 | |
| 726 def unhandledError(self, failure): | |
| 727 """ | |
| 728 This is a terminal callback called after application code has had a | |
| 729 chance to quash any errors. | |
| 730 """ | |
| 731 return self.boxSender.unhandledError(failure) | |
| 732 | |
| 733 | |
| 734 def _answerReceived(self, box): | |
| 735 """ | |
| 736 An AMP box was received that answered a command previously sent with | |
| 737 L{callRemote}. | |
| 738 | |
| 739 @param box: an AmpBox with a value for its L{ANSWER} key. | |
| 740 """ | |
| 741 question = self._outstandingRequests.pop(box[ANSWER]) | |
| 742 question.addErrback(self.unhandledError) | |
| 743 question.callback(box) | |
| 744 | |
| 745 | |
| 746 def _errorReceived(self, box): | |
| 747 """ | |
| 748 An AMP box was received that answered a command previously sent with | |
| 749 L{callRemote}, with an error. | |
| 750 | |
| 751 @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE}, | |
| 752 and L{ERROR_DESCRIPTION} keys. | |
| 753 """ | |
| 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) | |
| 760 else: | |
| 761 exc = RemoteAmpError(errorCode, description) | |
| 762 question.errback(Failure(exc)) | |
| 763 | |
| 764 | |
| 765 def _commandReceived(self, box): | |
| 766 """ | |
| 767 @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK} | |
| 768 keys. | |
| 769 """ | |
| 770 cmd = box[COMMAND] | |
| 771 def formatAnswer(answerBox): | |
| 772 answerBox[ANSWER] = box[ASK] | |
| 773 return answerBox | |
| 774 def formatError(error): | |
| 775 if error.check(RemoteAmpError): | |
| 776 code = error.value.errorCode | |
| 777 desc = error.value.description | |
| 778 if error.value.fatal: | |
| 779 errorBox = QuitBox() | |
| 780 else: | |
| 781 errorBox = AmpBox() | |
| 782 else: | |
| 783 errorBox = QuitBox() | |
| 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 | |
| 791 return errorBox | |
| 792 deferred = self.dispatchCommand(box) | |
| 793 if ASK in box: | |
| 794 deferred.addCallbacks(formatAnswer, formatError) | |
| 795 deferred.addCallback(self._safeEmit) | |
| 796 deferred.addErrback(self.unhandledError) | |
| 797 | |
| 798 | |
| 799 def ampBoxReceived(self, box): | |
| 800 """ | |
| 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. | |
| 804 | |
| 805 @param box: an AmpBox | |
| 806 | |
| 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. | |
| 810 """ | |
| 811 if ANSWER in box: | |
| 812 self._answerReceived(box) | |
| 813 elif ERROR in box: | |
| 814 self._errorReceived(box) | |
| 815 elif COMMAND in box: | |
| 816 self._commandReceived(box) | |
| 817 else: | |
| 818 raise NoEmptyBoxes(box) | |
| 819 | |
| 820 | |
| 821 def _safeEmit(self, aBox): | |
| 822 """ | |
| 823 Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors | |
| 824 which cannot be usefully handled. | |
| 825 """ | |
| 826 try: | |
| 827 aBox._sendTo(self.boxSender) | |
| 828 except (ProtocolSwitched, ConnectionLost): | |
| 829 pass | |
| 830 | |
| 831 | |
| 832 def dispatchCommand(self, box): | |
| 833 """ | |
| 834 A box with a _command key was received. | |
| 835 | |
| 836 Dispatch it to a local handler call it. | |
| 837 | |
| 838 @param proto: an AMP instance. | |
| 839 @param box: an AmpBox to be dispatched. | |
| 840 """ | |
| 841 cmd = box[COMMAND] | |
| 842 responder = self.locator.locateResponder(cmd) | |
| 843 if responder is None: | |
| 844 return fail(RemoteAmpError( | |
| 845 UNHANDLED_ERROR_CODE, | |
| 846 "Unhandled Command: %r" % (cmd,), | |
| 847 False, | |
| 848 local=Failure(UnhandledCommand()))) | |
| 849 return maybeDeferred(responder, box) | |
| 850 | |
| 851 | |
| 852 | |
| 853 class CommandLocator: | |
| 854 """ | |
| 855 A L{CommandLocator} is a collection of responders to AMP L{Command}s, with | |
| 856 the help of the L{Command.responder} decorator. | |
| 857 """ | |
| 858 | |
| 859 class __metaclass__(type): | |
| 860 """ | |
| 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 | |
| 867 class declaration. | |
| 868 | |
| 869 The Command.responder decorator explicitly cooperates with this | |
| 870 metaclass. | |
| 871 """ | |
| 872 | |
| 873 _currentClassCommands = [] | |
| 874 def __new__(cls, name, bases, attrs): | |
| 875 commands = cls._currentClassCommands[:] | |
| 876 cls._currentClassCommands[:] = [] | |
| 877 cd = attrs['_commandDispatch'] = {} | |
| 878 for base in bases: | |
| 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) | |
| 883 if (bases and ( | |
| 884 subcls.lookupFunction != CommandLocator.lookupFunction)): | |
| 885 def locateResponder(self, name): | |
| 886 warnings.warn( | |
| 887 "Override locateResponder, not lookupFunction.", | |
| 888 category=PendingDeprecationWarning, | |
| 889 stacklevel=2) | |
| 890 return self.lookupFunction(name) | |
| 891 subcls.locateResponder = locateResponder | |
| 892 return subcls | |
| 893 | |
| 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) | |
| 900 | |
| 901 implements(IResponderLocator) | |
| 902 | |
| 903 | |
| 904 def _wrapWithSerialization(self, aCallable, command): | |
| 905 """ | |
| 906 Wrap aCallable with its command's argument de-serialization | |
| 907 and result serialization logic. | |
| 908 | |
| 909 @param aCallable: a callable with a 'command' attribute, designed to be | |
| 910 called with keyword arguments. | |
| 911 | |
| 912 @param command: the command class whose serialization to use. | |
| 913 | |
| 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 | |
| 917 fails with an error. | |
| 918 """ | |
| 919 def doit(box): | |
| 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): | |
| 928 try: | |
| 929 return command.makeResponse(objects, self) | |
| 930 except: | |
| 931 # let's helpfully log this. | |
| 932 originalFailure = Failure() | |
| 933 raise BadLocalReturn( | |
| 934 "%r returned %r and %r could not serialize it" % ( | |
| 935 aCallable, | |
| 936 objects, | |
| 937 command), | |
| 938 originalFailure) | |
| 939 return maybeDeferred(aCallable, **kw).addCallback( | |
| 940 makeResponseFor).addErrback( | |
| 941 checkKnownErrors) | |
| 942 return doit | |
| 943 | |
| 944 | |
| 945 def lookupFunction(self, name): | |
| 946 """ | |
| 947 Deprecated synonym for L{locateResponder} | |
| 948 """ | |
| 949 if self.__class__.lookupFunction != CommandLocator.lookupFunction: | |
| 950 return CommandLocator.locateResponder(self, name) | |
| 951 else: | |
| 952 warnings.warn("Call locateResponder, not lookupFunction.", | |
| 953 category=PendingDeprecationWarning, | |
| 954 stacklevel=2) | |
| 955 return self.locateResponder(name) | |
| 956 | |
| 957 | |
| 958 def locateResponder(self, name): | |
| 959 """ | |
| 960 Locate a callable to invoke when executing the named command. | |
| 961 | |
| 962 @param name: the normalized name (from the wire) of the command. | |
| 963 | |
| 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. | |
| 967 """ | |
| 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 | |
| 971 if name in cd: | |
| 972 commandClass, responderFunc = cd[name] | |
| 973 responderMethod = types.MethodType( | |
| 974 responderFunc, self, self.__class__) | |
| 975 return self._wrapWithSerialization(responderMethod, commandClass) | |
| 976 | |
| 977 | |
| 978 | |
| 979 class SimpleStringLocator(object): | |
| 980 """ | |
| 981 Implement the L{locateResponder} method to do simple, string-based | |
| 982 dispatch. | |
| 983 """ | |
| 984 | |
| 985 implements(IResponderLocator) | |
| 986 | |
| 987 baseDispatchPrefix = 'amp_' | |
| 988 | |
| 989 def locateResponder(self, name): | |
| 990 """ | |
| 991 Locate a callable to invoke when executing the named command. | |
| 992 | |
| 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. | |
| 996 | |
| 997 @param name: the normalized name (from the wire) of the command. | |
| 998 """ | |
| 999 fName = self.baseDispatchPrefix + (name.upper()) | |
| 1000 return getattr(self, fName, None) | |
| 1001 | |
| 1002 | |
| 1003 | |
| 1004 PYTHON_KEYWORDS = [ | |
| 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'] | |
| 1009 | |
| 1010 | |
| 1011 | |
| 1012 def _wireNameToPythonIdentifier(key): | |
| 1013 """ | |
| 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 | |
| 1017 underscores. | |
| 1018 | |
| 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 | |
| 1028 names. | |
| 1029 | |
| 1030 @param key: a str, looking something like 'foo-bar-baz' or 'from' | |
| 1031 | |
| 1032 @return: a str which is a valid python identifier, looking something like | |
| 1033 'foo_bar_baz' or 'From'. | |
| 1034 """ | |
| 1035 lkey = key.replace("-", "_") | |
| 1036 if lkey in PYTHON_KEYWORDS: | |
| 1037 return lkey.title() | |
| 1038 return lkey | |
| 1039 | |
| 1040 | |
| 1041 | |
| 1042 class Argument: | |
| 1043 """ | |
| 1044 Base-class of all objects that take values from Amp packets and convert | |
| 1045 them into objects for Python functions. | |
| 1046 """ | |
| 1047 optional = False | |
| 1048 | |
| 1049 | |
| 1050 def __init__(self, optional=False): | |
| 1051 """ | |
| 1052 Create an Argument. | |
| 1053 | |
| 1054 @param optional: a boolean indicating whether this argument can be | |
| 1055 omitted in the protocol. | |
| 1056 """ | |
| 1057 self.optional = optional | |
| 1058 | |
| 1059 | |
| 1060 def retrieve(self, d, name, proto): | |
| 1061 """ | |
| 1062 Retrieve the given key from the given dictionary, removing it if found. | |
| 1063 | |
| 1064 @param d: a dictionary. | |
| 1065 | |
| 1066 @param name: a key in L{d}. | |
| 1067 | |
| 1068 @param proto: an instance of an AMP. | |
| 1069 | |
| 1070 @raise KeyError: if I am not optional and no value was found. | |
| 1071 | |
| 1072 @return: d[name]. | |
| 1073 """ | |
| 1074 if self.optional: | |
| 1075 value = d.get(name) | |
| 1076 if value is not None: | |
| 1077 del d[name] | |
| 1078 else: | |
| 1079 value = d.pop(name) | |
| 1080 return value | |
| 1081 | |
| 1082 | |
| 1083 def fromBox(self, name, strings, objects, proto): | |
| 1084 """ | |
| 1085 Populate an 'out' dictionary with mapping names to Python values | |
| 1086 decoded from an 'in' AmpBox mapping strings to string values. | |
| 1087 | |
| 1088 @param name: the argument name to retrieve | |
| 1089 @type name: str | |
| 1090 | |
| 1091 @param strings: The AmpBox to read string(s) from, a mapping of | |
| 1092 argument names to string values. | |
| 1093 @type strings: AmpBox | |
| 1094 | |
| 1095 @param objects: The dictionary to write object(s) to, a mapping of | |
| 1096 names to Python objects. | |
| 1097 @type objects: dict | |
| 1098 | |
| 1099 @param proto: an AMP instance. | |
| 1100 """ | |
| 1101 st = self.retrieve(strings, name, proto) | |
| 1102 nk = _wireNameToPythonIdentifier(name) | |
| 1103 if self.optional and st is None: | |
| 1104 objects[nk] = None | |
| 1105 else: | |
| 1106 objects[nk] = self.fromStringProto(st, proto) | |
| 1107 | |
| 1108 | |
| 1109 def toBox(self, name, strings, objects, proto): | |
| 1110 """ | |
| 1111 Populate an 'out' AmpBox with strings encoded from an 'in' dictionary | |
| 1112 mapping names to Python values. | |
| 1113 | |
| 1114 @param name: the argument name to retrieve | |
| 1115 @type name: str | |
| 1116 | |
| 1117 @param strings: The AmpBox to write string(s) to, a mapping of | |
| 1118 argument names to string values. | |
| 1119 @type strings: AmpBox | |
| 1120 | |
| 1121 @param objects: The dictionary to read object(s) from, a mapping of | |
| 1122 names to Python objects. | |
| 1123 | |
| 1124 @type objects: dict | |
| 1125 | |
| 1126 @param proto: the protocol we are converting for. | |
| 1127 @type proto: AMP | |
| 1128 """ | |
| 1129 obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto) | |
| 1130 if self.optional and obj is None: | |
| 1131 # strings[name] = None | |
| 1132 pass | |
| 1133 else: | |
| 1134 strings[name] = self.toStringProto(obj, proto) | |
| 1135 | |
| 1136 | |
| 1137 def fromStringProto(self, inString, proto): | |
| 1138 """ | |
| 1139 Convert a string to a Python value. | |
| 1140 | |
| 1141 @param inString: the string to convert. | |
| 1142 | |
| 1143 @param proto: the protocol we are converting for. | |
| 1144 @type proto: AMP | |
| 1145 | |
| 1146 @return: a Python object. | |
| 1147 """ | |
| 1148 return self.fromString(inString) | |
| 1149 | |
| 1150 | |
| 1151 def toStringProto(self, inObject, proto): | |
| 1152 """ | |
| 1153 Convert a Python object to a string. | |
| 1154 | |
| 1155 @param inObject: the object to convert. | |
| 1156 | |
| 1157 @param proto: the protocol we are converting for. | |
| 1158 @type proto: AMP | |
| 1159 """ | |
| 1160 return self.toString(inObject) | |
| 1161 | |
| 1162 | |
| 1163 def fromString(self, inString): | |
| 1164 """ | |
| 1165 Convert a string to a Python object. Subclasses must implement this. | |
| 1166 | |
| 1167 @param inString: the string to convert. | |
| 1168 @type inString: str | |
| 1169 | |
| 1170 @return: the decoded value from inString | |
| 1171 """ | |
| 1172 | |
| 1173 | |
| 1174 def toString(self, inObject): | |
| 1175 """ | |
| 1176 Convert a Python object into a string for passing over the network. | |
| 1177 | |
| 1178 @param inObject: an object of the type that this Argument is intended | |
| 1179 to deal with. | |
| 1180 | |
| 1181 @return: the wire encoding of inObject | |
| 1182 @rtype: str | |
| 1183 """ | |
| 1184 | |
| 1185 | |
| 1186 | |
| 1187 class Integer(Argument): | |
| 1188 """ | |
| 1189 Convert to and from 'int'. | |
| 1190 """ | |
| 1191 fromString = int | |
| 1192 def toString(self, inObject): | |
| 1193 return str(int(inObject)) | |
| 1194 | |
| 1195 | |
| 1196 | |
| 1197 class String(Argument): | |
| 1198 """ | |
| 1199 Don't do any conversion at all; just pass through 'str'. | |
| 1200 """ | |
| 1201 def toString(self, inObject): | |
| 1202 return inObject | |
| 1203 | |
| 1204 | |
| 1205 def fromString(self, inString): | |
| 1206 return inString | |
| 1207 | |
| 1208 | |
| 1209 | |
| 1210 class Float(Argument): | |
| 1211 """ | |
| 1212 Encode floating-point values on the wire as their repr. | |
| 1213 """ | |
| 1214 fromString = float | |
| 1215 toString = repr | |
| 1216 | |
| 1217 | |
| 1218 | |
| 1219 class Boolean(Argument): | |
| 1220 """ | |
| 1221 Encode True or False as "True" or "False" on the wire. | |
| 1222 """ | |
| 1223 def fromString(self, inString): | |
| 1224 if inString == 'True': | |
| 1225 return True | |
| 1226 elif inString == 'False': | |
| 1227 return False | |
| 1228 else: | |
| 1229 raise TypeError("Bad boolean value: %r" % (inString,)) | |
| 1230 | |
| 1231 | |
| 1232 def toString(self, inObject): | |
| 1233 if inObject: | |
| 1234 return 'True' | |
| 1235 else: | |
| 1236 return 'False' | |
| 1237 | |
| 1238 | |
| 1239 | |
| 1240 class Unicode(String): | |
| 1241 """ | |
| 1242 Encode a unicode string on the wire as UTF-8. | |
| 1243 """ | |
| 1244 | |
| 1245 def toString(self, inObject): | |
| 1246 # assert isinstance(inObject, unicode) | |
| 1247 return String.toString(self, inObject.encode('utf-8')) | |
| 1248 | |
| 1249 | |
| 1250 def fromString(self, inString): | |
| 1251 # assert isinstance(inString, str) | |
| 1252 return String.fromString(self, inString).decode('utf-8') | |
| 1253 | |
| 1254 | |
| 1255 | |
| 1256 class Path(Unicode): | |
| 1257 """ | |
| 1258 Encode and decode L{filepath.FilePath} instances as paths on the wire. | |
| 1259 | |
| 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. | |
| 1264 """ | |
| 1265 def fromString(self, inString): | |
| 1266 return filepath.FilePath(Unicode.fromString(self, inString)) | |
| 1267 | |
| 1268 | |
| 1269 def toString(self, inObject): | |
| 1270 return Unicode.toString(self, inObject.path) | |
| 1271 | |
| 1272 | |
| 1273 | |
| 1274 class AmpList(Argument): | |
| 1275 """ | |
| 1276 Convert a list of dictionaries into a list of AMP boxes on the wire. | |
| 1277 | |
| 1278 For example, if you want to pass:: | |
| 1279 | |
| 1280 [{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}] | |
| 1281 | |
| 1282 You might use an AmpList like this in your arguments or response list:: | |
| 1283 | |
| 1284 AmpList([('a', Integer()), | |
| 1285 ('b', Unicode())]) | |
| 1286 """ | |
| 1287 def __init__(self, subargs): | |
| 1288 """ | |
| 1289 Create an AmpList. | |
| 1290 | |
| 1291 @param subargs: a list of 2-tuples of ('name', argument) describing the | |
| 1292 schema of the dictionaries in the sequence of amp boxes. | |
| 1293 """ | |
| 1294 self.subargs = subargs | |
| 1295 | |
| 1296 | |
| 1297 def fromStringProto(self, inString, proto): | |
| 1298 boxes = parseString(inString) | |
| 1299 values = [_stringsToObjects(box, self.subargs, proto) | |
| 1300 for box in boxes] | |
| 1301 return values | |
| 1302 | |
| 1303 | |
| 1304 def toStringProto(self, inObject, proto): | |
| 1305 return ''.join([_objectsToStrings( | |
| 1306 objects, self.subargs, Box(), proto | |
| 1307 ).serialize() for objects in inObject]) | |
| 1308 | |
| 1309 class Command: | |
| 1310 """ | |
| 1311 Subclass me to specify an AMP Command. | |
| 1312 | |
| 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 | |
| 1315 this command. | |
| 1316 | |
| 1317 @cvar response: A list like L{arguments}, but instead used for the return | |
| 1318 value. | |
| 1319 | |
| 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. | |
| 1330 | |
| 1331 @cvar fatalErrors: like 'errors', but errors in this list will always | |
| 1332 terminate the connection, despite being of a recognizable error type. | |
| 1333 | |
| 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}. | |
| 1337 | |
| 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}. | |
| 1341 | |
| 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 | |
| 1347 want one. | |
| 1348 """ | |
| 1349 | |
| 1350 class __metaclass__(type): | |
| 1351 """ | |
| 1352 Metaclass hack to establish reverse-mappings for 'errors' and | |
| 1353 'fatalErrors' as class vars. | |
| 1354 """ | |
| 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) | |
| 1361 errors = {} | |
| 1362 fatalErrors = {} | |
| 1363 accumulateClassDict(newtype, 'errors', errors) | |
| 1364 accumulateClassDict(newtype, 'fatalErrors', fatalErrors) | |
| 1365 for v, k in errors.iteritems(): | |
| 1366 re[k] = v | |
| 1367 er[v] = k | |
| 1368 for v, k in fatalErrors.iteritems(): | |
| 1369 re[k] = v | |
| 1370 er[v] = k | |
| 1371 return newtype | |
| 1372 | |
| 1373 arguments = [] | |
| 1374 response = [] | |
| 1375 extra = [] | |
| 1376 errors = {} | |
| 1377 fatalErrors = {} | |
| 1378 | |
| 1379 commandType = Box | |
| 1380 responseType = Box | |
| 1381 | |
| 1382 requiresAnswer = True | |
| 1383 | |
| 1384 | |
| 1385 def __init__(self, **kw): | |
| 1386 """ | |
| 1387 Create an instance of this command with specified values for its | |
| 1388 parameters. | |
| 1389 | |
| 1390 @param kw: a dict containing an appropriate value for each name | |
| 1391 specified in the L{arguments} attribute of my class. | |
| 1392 | |
| 1393 @raise InvalidSignature: if you forgot any required arguments. | |
| 1394 """ | |
| 1395 self.structured = kw | |
| 1396 givenArgs = kw.keys() | |
| 1397 forgotten = [] | |
| 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) | |
| 1402 if forgotten: | |
| 1403 raise InvalidSignature("forgot %s for %s" % ( | |
| 1404 ', '.join(forgotten), self.commandName)) | |
| 1405 forgotten = [] | |
| 1406 | |
| 1407 | |
| 1408 def makeResponse(cls, objects, proto): | |
| 1409 """ | |
| 1410 Serialize a mapping of arguments using this L{Command}'s | |
| 1411 response schema. | |
| 1412 | |
| 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. | |
| 1416 | |
| 1417 @param proto: an L{AMP}. | |
| 1418 | |
| 1419 @return: an L{AmpBox}. | |
| 1420 """ | |
| 1421 return _objectsToStrings(objects, cls.response, cls.responseType(), | |
| 1422 proto) | |
| 1423 makeResponse = classmethod(makeResponse) | |
| 1424 | |
| 1425 | |
| 1426 def makeArguments(cls, objects, proto): | |
| 1427 """ | |
| 1428 Serialize a mapping of arguments using this L{Command}'s | |
| 1429 argument schema. | |
| 1430 | |
| 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. | |
| 1434 | |
| 1435 @param proto: an L{AMP}. | |
| 1436 | |
| 1437 @return: An instance of this L{Command}'s C{commandType}. | |
| 1438 """ | |
| 1439 return _objectsToStrings(objects, cls.arguments, cls.commandType(), | |
| 1440 proto) | |
| 1441 makeArguments = classmethod(makeArguments) | |
| 1442 | |
| 1443 | |
| 1444 def parseResponse(cls, box, protocol): | |
| 1445 """ | |
| 1446 Parse a mapping of serialized arguments using this | |
| 1447 L{Command}'s response schema. | |
| 1448 | |
| 1449 @param box: A mapping of response-argument names to the | |
| 1450 serialized forms of those arguments. | |
| 1451 @param protocol: The L{AMP} protocol. | |
| 1452 | |
| 1453 @return: A mapping of response-argument names to the parsed | |
| 1454 forms. | |
| 1455 """ | |
| 1456 return _stringsToObjects(box, cls.response, protocol) | |
| 1457 parseResponse = classmethod(parseResponse) | |
| 1458 | |
| 1459 | |
| 1460 def parseArguments(cls, box, protocol): | |
| 1461 """ | |
| 1462 Parse a mapping of serialized arguments using this | |
| 1463 L{Command}'s argument schema. | |
| 1464 | |
| 1465 @param box: A mapping of argument names to the seralized forms | |
| 1466 of those arguments. | |
| 1467 @param protocol: The L{AMP} protocol. | |
| 1468 | |
| 1469 @return: A mapping of argument names to the parsed forms. | |
| 1470 """ | |
| 1471 return _stringsToObjects(box, cls.arguments, protocol) | |
| 1472 parseArguments = classmethod(parseArguments) | |
| 1473 | |
| 1474 | |
| 1475 def responder(cls, methodfunc): | |
| 1476 """ | |
| 1477 Declare a method to be a responder for a particular command. | |
| 1478 | |
| 1479 This is a decorator. | |
| 1480 | |
| 1481 Use like so:: | |
| 1482 | |
| 1483 class MyCommand(Command): | |
| 1484 arguments = [('a', ...), ('b', ...)] | |
| 1485 | |
| 1486 class MyProto(AMP): | |
| 1487 def myFunMethod(self, a, b): | |
| 1488 ... | |
| 1489 MyCommand.responder(myFunMethod) | |
| 1490 | |
| 1491 Notes: Although decorator syntax is not used within Twisted, this | |
| 1492 function returns its argument and is therefore safe to use with | |
| 1493 decorator syntax. | |
| 1494 | |
| 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. | |
| 1498 | |
| 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. | |
| 1503 | |
| 1504 @return: the methodfunc parameter. | |
| 1505 """ | |
| 1506 CommandLocator._currentClassCommands.append((cls, methodfunc)) | |
| 1507 return methodfunc | |
| 1508 responder = classmethod(responder) | |
| 1509 | |
| 1510 | |
| 1511 # Our only instance method | |
| 1512 def _doCommand(self, proto): | |
| 1513 """ | |
| 1514 Encode and send this Command to the given protocol. | |
| 1515 | |
| 1516 @param proto: an AMP, representing the connection to send to. | |
| 1517 | |
| 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). | |
| 1521 """ | |
| 1522 | |
| 1523 def _massageError(error): | |
| 1524 error.trap(RemoteAmpError) | |
| 1525 rje = error.value | |
| 1526 errorType = self.reverseErrors.get(rje.errorCode, | |
| 1527 UnknownRemoteError) | |
| 1528 return Failure(errorType(rje.description)) | |
| 1529 | |
| 1530 d = proto._sendBoxCommand(self.commandName, | |
| 1531 self.makeArguments(self.structured, proto), | |
| 1532 self.requiresAnswer) | |
| 1533 | |
| 1534 if self.requiresAnswer: | |
| 1535 d.addCallback(self.parseResponse, proto) | |
| 1536 d.addErrback(_massageError) | |
| 1537 | |
| 1538 return d | |
| 1539 | |
| 1540 | |
| 1541 | |
| 1542 class _NoCertificate: | |
| 1543 """ | |
| 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. | |
| 1548 | |
| 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 | |
| 1552 that. | |
| 1553 """ | |
| 1554 | |
| 1555 def __init__(self, client): | |
| 1556 """ | |
| 1557 Create a _NoCertificate which either is or isn't for the client side of | |
| 1558 the connection. | |
| 1559 | |
| 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. | |
| 1563 | |
| 1564 @type client: bool | |
| 1565 """ | |
| 1566 self.client = client | |
| 1567 | |
| 1568 | |
| 1569 def options(self, *authorities): | |
| 1570 """ | |
| 1571 Behaves like L{twisted.internet.ssl.PrivateCertificate.options}(). | |
| 1572 """ | |
| 1573 if not self.client: | |
| 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. | |
| 1577 | |
| 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) | |
| 1586 options = dict() | |
| 1587 if authorities: | |
| 1588 options.update(dict(verify=True, | |
| 1589 requireCertificate=True, | |
| 1590 caCerts=[auth.original for auth in authorities])
) | |
| 1591 occo = CertificateOptions(**options) | |
| 1592 return occo | |
| 1593 | |
| 1594 | |
| 1595 | |
| 1596 class _TLSBox(AmpBox): | |
| 1597 """ | |
| 1598 I am an AmpBox that, upon being sent, initiates a TLS connection. | |
| 1599 """ | |
| 1600 __slots__ = [] | |
| 1601 | |
| 1602 def _keyprop(k, default): | |
| 1603 return property(lambda self: self.get(k, default)) | |
| 1604 | |
| 1605 | |
| 1606 # These properties are described in startTLS | |
| 1607 certificate = _keyprop('tls_localCertificate', _NoCertificate(False)) | |
| 1608 verify = _keyprop('tls_verifyAuthorities', None) | |
| 1609 | |
| 1610 def _sendTo(self, proto): | |
| 1611 """ | |
| 1612 Send my encoded value to the protocol, then initiate TLS. | |
| 1613 """ | |
| 1614 ab = AmpBox(self) | |
| 1615 for k in ['tls_localCertificate', | |
| 1616 'tls_verifyAuthorities']: | |
| 1617 ab.pop(k, None) | |
| 1618 ab._sendTo(proto) | |
| 1619 proto._startTLS(self.certificate, self.verify) | |
| 1620 | |
| 1621 | |
| 1622 | |
| 1623 class _LocalArgument(String): | |
| 1624 """ | |
| 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. | |
| 1628 """ | |
| 1629 | |
| 1630 def fromBox(self, name, strings, objects, proto): | |
| 1631 pass | |
| 1632 | |
| 1633 | |
| 1634 | |
| 1635 class StartTLS(Command): | |
| 1636 """ | |
| 1637 Use, or subclass, me to implement a command that starts TLS. | |
| 1638 | |
| 1639 Callers of StartTLS may pass several special arguments, which affect the | |
| 1640 TLS negotiation: | |
| 1641 | |
| 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. | |
| 1645 | |
| 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. | |
| 1649 | |
| 1650 Each of those special parameters may also be present as a key in the | |
| 1651 response dictionary. | |
| 1652 """ | |
| 1653 | |
| 1654 arguments = [("tls_localCertificate", _LocalArgument(optional=True)), | |
| 1655 ("tls_verifyAuthorities", _LocalArgument(optional=True))] | |
| 1656 | |
| 1657 response = [("tls_localCertificate", _LocalArgument(optional=True)), | |
| 1658 ("tls_verifyAuthorities", _LocalArgument(optional=True))] | |
| 1659 | |
| 1660 responseType = _TLSBox | |
| 1661 | |
| 1662 def __init__(self, **kw): | |
| 1663 """ | |
| 1664 Create a StartTLS command. (This is private. Use AMP.callRemote.) | |
| 1665 | |
| 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. | |
| 1669 | |
| 1670 @param tls_verifyAuthorities: a list of Certificate objects which | |
| 1671 represent root certificates to verify our peer with. | |
| 1672 """ | |
| 1673 self.certificate = kw.pop('tls_localCertificate', _NoCertificate(True)) | |
| 1674 self.authorities = kw.pop('tls_verifyAuthorities', None) | |
| 1675 Command.__init__(self, **kw) | |
| 1676 | |
| 1677 | |
| 1678 def _doCommand(self, proto): | |
| 1679 """ | |
| 1680 When a StartTLS command is sent, prepare to start TLS, but don't actuall
y | |
| 1681 do it; wait for the acknowledgement, then initiate the TLS handshake. | |
| 1682 """ | |
| 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) | |
| 1688 return response | |
| 1689 d.addCallback(actuallystart) | |
| 1690 return d | |
| 1691 | |
| 1692 | |
| 1693 | |
| 1694 class ProtocolSwitchCommand(Command): | |
| 1695 """ | |
| 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 | |
| 1701 remain secured. | |
| 1702 """ | |
| 1703 | |
| 1704 def __init__(self, _protoToSwitchToFactory, **kw): | |
| 1705 """ | |
| 1706 Create a ProtocolSwitchCommand. | |
| 1707 | |
| 1708 @param _protoToSwitchToFactory: a ProtocolFactory which will generate | |
| 1709 the Protocol to switch to. | |
| 1710 | |
| 1711 @param kw: Keyword arguments, encoded and handled normally as | |
| 1712 L{Command} would. | |
| 1713 """ | |
| 1714 | |
| 1715 self.protoToSwitchToFactory = _protoToSwitchToFactory | |
| 1716 super(ProtocolSwitchCommand, self).__init__(**kw) | |
| 1717 | |
| 1718 | |
| 1719 def makeResponse(cls, innerProto, proto): | |
| 1720 return _SwitchBox(innerProto) | |
| 1721 makeResponse = classmethod(makeResponse) | |
| 1722 | |
| 1723 | |
| 1724 def _doCommand(self, proto): | |
| 1725 """ | |
| 1726 When we emit a ProtocolSwitchCommand, lock the protocol, but don't actua
lly | |
| 1727 switch to the new protocol unless an acknowledgement is received. If | |
| 1728 an error is received, switch back. | |
| 1729 """ | |
| 1730 d = super(ProtocolSwitchCommand, self)._doCommand(proto) | |
| 1731 proto._lockForSwitch() | |
| 1732 def switchNow(ign): | |
| 1733 innerProto = self.protoToSwitchToFactory.buildProtocol( | |
| 1734 proto.transport.getPeer()) | |
| 1735 proto._switchTo(innerProto, self.protoToSwitchToFactory) | |
| 1736 return ign | |
| 1737 def handle(ign): | |
| 1738 proto._unlockFromSwitch() | |
| 1739 self.protoToSwitchToFactory.clientConnectionFailed( | |
| 1740 None, Failure(CONNECTION_LOST)) | |
| 1741 return ign | |
| 1742 return d.addCallbacks(switchNow, handle) | |
| 1743 | |
| 1744 | |
| 1745 | |
| 1746 class BinaryBoxProtocol(StatefulStringProtocol, Int16StringReceiver): | |
| 1747 """ | |
| 1748 A protocol for receving L{Box}es - key/value pairs - via length-prefixed | |
| 1749 strings. A box is composed of: | |
| 1750 | |
| 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 | |
| 1757 of 65535) | |
| 1758 - a value, comprised of that many bytes | |
| 1759 - 2 null bytes | |
| 1760 | |
| 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. | |
| 1763 | |
| 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}. | |
| 1769 | |
| 1770 @ivar boxReceiver: an L{IBoxReceiver} provider, whose L{ampBoxReceived} | |
| 1771 method will be invoked for each L{Box} that is received. | |
| 1772 """ | |
| 1773 | |
| 1774 implements(IBoxSender) | |
| 1775 | |
| 1776 _justStartedTLS = False | |
| 1777 _startingTLSBuffer = None | |
| 1778 _locked = False | |
| 1779 _currentKey = None | |
| 1780 _currentBox = None | |
| 1781 | |
| 1782 hostCertificate = None | |
| 1783 noPeerCertificate = False # for tests | |
| 1784 innerProtocol = None | |
| 1785 innerProtocolClientFactory = None | |
| 1786 | |
| 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 | |
| 1793 | |
| 1794 | |
| 1795 def __init__(self, boxReceiver): | |
| 1796 self.boxReceiver = boxReceiver | |
| 1797 | |
| 1798 | |
| 1799 def _switchTo(self, newProto, clientFactory=None): | |
| 1800 """ | |
| 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. | |
| 1804 | |
| 1805 @param newProto: the new protocol instance to switch to. | |
| 1806 | |
| 1807 @param clientFactory: the ClientFactory to send the | |
| 1808 L{clientConnectionLost} notification to. | |
| 1809 """ | |
| 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 | |
| 1817 # length prefix. | |
| 1818 self.recvd = '' | |
| 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) | |
| 1825 | |
| 1826 | |
| 1827 def sendBox(self, box): | |
| 1828 """ | |
| 1829 Send a amp.Box to my peer. | |
| 1830 | |
| 1831 Note: transport.write is never called outside of this method. | |
| 1832 | |
| 1833 @param box: an AmpBox. | |
| 1834 | |
| 1835 @raise ProtocolSwitched: if the protocol has previously been switched. | |
| 1836 | |
| 1837 @raise ConnectionLost: if the connection has previously been lost. | |
| 1838 """ | |
| 1839 if self._locked: | |
| 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) | |
| 1846 else: | |
| 1847 self.transport.write(box.serialize()) | |
| 1848 | |
| 1849 | |
| 1850 def makeConnection(self, transport): | |
| 1851 """ | |
| 1852 Notify L{boxReceiver} that it is about to receive boxes from this | |
| 1853 protocol by invoking L{startReceivingBoxes}. | |
| 1854 """ | |
| 1855 self.boxReceiver.startReceivingBoxes(self) | |
| 1856 Int16StringReceiver.makeConnection(self, transport) | |
| 1857 | |
| 1858 | |
| 1859 def dataReceived(self, data): | |
| 1860 """ | |
| 1861 Either parse incoming data as L{AmpBox}es or relay it to our nested | |
| 1862 protocol. | |
| 1863 """ | |
| 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) | |
| 1870 return | |
| 1871 return Int16StringReceiver.dataReceived(self, data) | |
| 1872 | |
| 1873 | |
| 1874 def connectionLost(self, reason): | |
| 1875 """ | |
| 1876 The connection was lost; notify any nested protocol. | |
| 1877 """ | |
| 1878 if self.innerProtocol is not None: | |
| 1879 self.innerProtocol.connectionLost(reason) | |
| 1880 if self.innerProtocolClientFactory is not None: | |
| 1881 self.innerProtocolClientFactory.clientConnectionLost(None, reaso
n) | |
| 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 | |
| 1888 if problems: | |
| 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 | |
| 1894 # error.) | |
| 1895 failReason = PeerVerifyError( | |
| 1896 "Peer rejected our certificate for an unknown reason.") | |
| 1897 else: | |
| 1898 failReason = reason | |
| 1899 self.boxReceiver.stopReceivingBoxes(failReason) | |
| 1900 | |
| 1901 | |
| 1902 | |
| 1903 def proto_init(self, string): | |
| 1904 """ | |
| 1905 String received in the 'init' state. | |
| 1906 """ | |
| 1907 self._currentBox = AmpBox() | |
| 1908 return self.proto_key(string) | |
| 1909 | |
| 1910 | |
| 1911 def proto_key(self, string): | |
| 1912 """ | |
| 1913 String received in the 'key' state. If the key is empty, a complete | |
| 1914 box has been received. | |
| 1915 """ | |
| 1916 if string: | |
| 1917 self._currentKey = string | |
| 1918 return 'value' | |
| 1919 else: | |
| 1920 self.boxReceiver.ampBoxReceived(self._currentBox) | |
| 1921 self._currentBox = None | |
| 1922 return 'init' | |
| 1923 | |
| 1924 | |
| 1925 def proto_value(self, string): | |
| 1926 """ | |
| 1927 String received in the 'value' state. | |
| 1928 """ | |
| 1929 self._currentBox[self._currentKey] = string | |
| 1930 self._currentKey = None | |
| 1931 return 'key' | |
| 1932 | |
| 1933 | |
| 1934 def _lockForSwitch(self): | |
| 1935 """ | |
| 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 | |
| 1939 this directly. | |
| 1940 """ | |
| 1941 self._locked = True | |
| 1942 | |
| 1943 | |
| 1944 def _unlockFromSwitch(self): | |
| 1945 """ | |
| 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 | |
| 1948 for some reason. | |
| 1949 """ | |
| 1950 if self.innerProtocol is not None: | |
| 1951 raise ProtocolSwitched("Protocol already switched. Cannot unlock.") | |
| 1952 self._locked = False | |
| 1953 | |
| 1954 | |
| 1955 def _prepareTLS(self, certificate, verifyAuthorities): | |
| 1956 """ | |
| 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 | |
| 1959 L{_sendBox}. | |
| 1960 """ | |
| 1961 self._startingTLSBuffer = [] | |
| 1962 if self.hostCertificate is not None: | |
| 1963 raise OnlyOneTLS( | |
| 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))) | |
| 1969 | |
| 1970 | |
| 1971 def _startTLS(self, certificate, verifyAuthorities): | |
| 1972 """ | |
| 1973 Used by TLSBox to initiate the SSL handshake. | |
| 1974 | |
| 1975 @param certificate: a L{twisted.internet.ssl.PrivateCertificate} for | |
| 1976 use locally. | |
| 1977 | |
| 1978 @param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances | |
| 1979 representing certificate authorities which will verify our peer. | |
| 1980 """ | |
| 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 | |
| 1993 for box in stlsb: | |
| 1994 self.sendBox(box) | |
| 1995 | |
| 1996 | |
| 1997 def _getPeerCertificate(self): | |
| 1998 if self.noPeerCertificate: | |
| 1999 return None | |
| 2000 return Certificate.peerFromTransport(self.transport) | |
| 2001 peerCertificate = property(_getPeerCertificate) | |
| 2002 | |
| 2003 | |
| 2004 def unhandledError(self, failure): | |
| 2005 """ | |
| 2006 The buck stops here. This error was completely unhandled, time to | |
| 2007 terminate the connection. | |
| 2008 """ | |
| 2009 log.msg("Amp server or network failure " | |
| 2010 "unhandled by client application:") | |
| 2011 log.err(failure) | |
| 2012 log.msg( | |
| 2013 "Dropping connection! " | |
| 2014 "To avoid, add errbacks to ALL remote commands!") | |
| 2015 if self.transport is not None: | |
| 2016 self.transport.loseConnection() | |
| 2017 | |
| 2018 | |
| 2019 def _defaultStartTLSResponder(self): | |
| 2020 """ | |
| 2021 The default TLS responder doesn't specify any certificate or anything. | |
| 2022 | |
| 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 | |
| 2025 convenience. | |
| 2026 | |
| 2027 You probably want to override this by providing your own StartTLS.respon
der. | |
| 2028 """ | |
| 2029 return {} | |
| 2030 StartTLS.responder(_defaultStartTLSResponder) | |
| 2031 | |
| 2032 | |
| 2033 | |
| 2034 class AMP(BinaryBoxProtocol, BoxDispatcher, | |
| 2035 CommandLocator, SimpleStringLocator): | |
| 2036 """ | |
| 2037 This protocol is an AMP connection. See the module docstring for protocol | |
| 2038 details. | |
| 2039 """ | |
| 2040 | |
| 2041 _ampInitialized = False | |
| 2042 | |
| 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: | |
| 2052 boxReceiver = self | |
| 2053 if locator is None: | |
| 2054 locator = self | |
| 2055 boxSender = self | |
| 2056 BoxDispatcher.__init__(self, locator) | |
| 2057 BinaryBoxProtocol.__init__(self, boxReceiver) | |
| 2058 | |
| 2059 | |
| 2060 def locateResponder(self, name): | |
| 2061 """ | |
| 2062 Unify the implementations of L{CommandLocator} and | |
| 2063 L{SimpleStringLocator} to perform both kinds of dispatch, preferring | |
| 2064 L{CommandLocator}. | |
| 2065 """ | |
| 2066 firstResponder = CommandLocator.locateResponder(self, name) | |
| 2067 if firstResponder is not None: | |
| 2068 return firstResponder | |
| 2069 secondResponder = SimpleStringLocator.locateResponder(self, name) | |
| 2070 return secondResponder | |
| 2071 | |
| 2072 | |
| 2073 def __repr__(self): | |
| 2074 """ | |
| 2075 A verbose string representation which gives us information about this | |
| 2076 AMP connection. | |
| 2077 """ | |
| 2078 return '<%s %s at 0x%x>' % ( | |
| 2079 self.__class__.__name__, | |
| 2080 self.innerProtocol, id(self)) | |
| 2081 | |
| 2082 | |
| 2083 def makeConnection(self, transport): | |
| 2084 """ | |
| 2085 Emit a helpful log message when the connection is made. | |
| 2086 """ | |
| 2087 if not self._ampInitialized: | |
| 2088 # See comment in the constructor re: backward compatibility. I | |
| 2089 # should probably emit a deprecation warning here. | |
| 2090 AMP.__init__(self) | |
| 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) | |
| 2099 | |
| 2100 | |
| 2101 def connectionLost(self, reason): | |
| 2102 """ | |
| 2103 Emit a helpful log message when the connection is lost. | |
| 2104 """ | |
| 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 | |
| 2111 | |
| 2112 | |
| 2113 | |
| 2114 class _ParserHelper: | |
| 2115 """ | |
| 2116 A box receiver which records all boxes received. | |
| 2117 """ | |
| 2118 def __init__(self): | |
| 2119 self.boxes = [] | |
| 2120 | |
| 2121 | |
| 2122 def getPeer(self): | |
| 2123 return 'string' | |
| 2124 | |
| 2125 | |
| 2126 def getHost(self): | |
| 2127 return 'string' | |
| 2128 | |
| 2129 disconnecting = False | |
| 2130 | |
| 2131 | |
| 2132 def startReceivingBoxes(self, sender): | |
| 2133 """ | |
| 2134 No initialization is required. | |
| 2135 """ | |
| 2136 | |
| 2137 | |
| 2138 def ampBoxReceived(self, box): | |
| 2139 self.boxes.append(box) | |
| 2140 | |
| 2141 | |
| 2142 # Synchronous helpers | |
| 2143 def parse(cls, fileObj): | |
| 2144 """ | |
| 2145 Parse some amp data stored in a file. | |
| 2146 | |
| 2147 @param fileObj: a file-like object. | |
| 2148 | |
| 2149 @return: a list of AmpBoxes encoded in the given file. | |
| 2150 """ | |
| 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) | |
| 2157 | |
| 2158 | |
| 2159 def parseString(cls, data): | |
| 2160 """ | |
| 2161 Parse some amp data stored in a string. | |
| 2162 | |
| 2163 @param data: a str holding some amp-encoded data. | |
| 2164 | |
| 2165 @return: a list of AmpBoxes encoded in the given string. | |
| 2166 """ | |
| 2167 return cls.parse(StringIO(data)) | |
| 2168 parseString = classmethod(parseString) | |
| 2169 | |
| 2170 | |
| 2171 | |
| 2172 parse = _ParserHelper.parse | |
| 2173 parseString = _ParserHelper.parseString | |
| 2174 | |
| 2175 def _stringsToObjects(strings, arglist, proto): | |
| 2176 """ | |
| 2177 Convert an AmpBox to a dictionary of python objects, converting through a | |
| 2178 given arglist. | |
| 2179 | |
| 2180 @param strings: an AmpBox (or dict of strings) | |
| 2181 | |
| 2182 @param arglist: a list of 2-tuples of strings and Argument objects, as | |
| 2183 described in L{Command.arguments}. | |
| 2184 | |
| 2185 @param proto: an L{AMP} instance. | |
| 2186 | |
| 2187 @return: the converted dictionary mapping names to argument objects. | |
| 2188 """ | |
| 2189 objects = {} | |
| 2190 myStrings = strings.copy() | |
| 2191 for argname, argparser in arglist: | |
| 2192 argparser.fromBox(argname, myStrings, objects, proto) | |
| 2193 return objects | |
| 2194 | |
| 2195 | |
| 2196 | |
| 2197 def _objectsToStrings(objects, arglist, strings, proto): | |
| 2198 """ | |
| 2199 Convert a dictionary of python objects to an AmpBox, converting through a | |
| 2200 given arglist. | |
| 2201 | |
| 2202 @param objects: a dict mapping names to python objects | |
| 2203 | |
| 2204 @param arglist: a list of 2-tuples of strings and Argument objects, as | |
| 2205 described in L{Command.arguments}. | |
| 2206 | |
| 2207 @param strings: [OUT PARAMETER] An object providing the L{dict} | |
| 2208 interface which will be populated with serialized data. | |
| 2209 | |
| 2210 @param proto: an L{AMP} instance. | |
| 2211 | |
| 2212 @return: The converted dictionary mapping names to encoded argument | |
| 2213 strings (identical to C{strings}). | |
| 2214 """ | |
| 2215 myObjects = {} | |
| 2216 for (k, v) in objects.items(): | |
| 2217 myObjects[k] = v | |
| 2218 | |
| 2219 for argname, argparser in arglist: | |
| 2220 argparser.toBox(argname, strings, myObjects, proto) | |
| 2221 return strings | |
| 2222 | |
| 2223 | |
| OLD | NEW |