1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830 |
- # -*- test-case-name: twisted.test.test_amp -*-
- # Copyright (c) 2005 Divmod, Inc.
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- """
- This module implements AMP, the Asynchronous Messaging Protocol.
-
- AMP is a protocol for sending multiple asynchronous request/response pairs over
- the same connection. Requests and responses are both collections of key/value
- pairs.
-
- AMP is a very simple protocol which is not an application. This module is a
- "protocol construction kit" of sorts; it attempts to be the simplest wire-level
- implementation of Deferreds. AMP provides the following base-level features:
-
- - Asynchronous request/response handling (hence the name)
-
- - Requests and responses are both key/value pairs
-
- - Binary transfer of all data: all data is length-prefixed. Your
- application will never need to worry about quoting.
-
- - Command dispatching (like HTTP Verbs): the protocol is extensible, and
- multiple AMP sub-protocols can be grouped together easily.
-
- The protocol implementation also provides a few additional features which are
- not part of the core wire protocol, but are nevertheless very useful:
-
- - Tight TLS integration, with an included StartTLS command.
-
- - Handshaking to other protocols: because AMP has well-defined message
- boundaries and maintains all incoming and outgoing requests for you, you
- can start a connection over AMP and then switch to another protocol.
- This makes it ideal for firewall-traversal applications where you may
- have only one forwarded port but multiple applications that want to use
- it.
-
- Using AMP with Twisted is simple. Each message is a command, with a response.
- You begin by defining a command type. Commands specify their input and output
- in terms of the types that they expect to see in the request and response
- key-value pairs. Here's an example of a command that adds two integers, 'a'
- and 'b'::
-
- class Sum(amp.Command):
- arguments = [('a', amp.Integer()),
- ('b', amp.Integer())]
- response = [('total', amp.Integer())]
-
- Once you have specified a command, you need to make it part of a protocol, and
- define a responder for it. Here's a 'JustSum' protocol that includes a
- responder for our 'Sum' command::
-
- class JustSum(amp.AMP):
- def sum(self, a, b):
- total = a + b
- print 'Did a sum: %d + %d = %d' % (a, b, total)
- return {'total': total}
- Sum.responder(sum)
-
- Later, when you want to actually do a sum, the following expression will return
- a L{Deferred} which will fire with the result::
-
- ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
- lambda p: p.callRemote(Sum, a=13, b=81)).addCallback(
- lambda result: result['total'])
-
- Command responders may also return Deferreds, causing the response to be
- sent only once the Deferred fires::
-
- class DelayedSum(amp.AMP):
- def slowSum(self, a, b):
- total = a + b
- result = defer.Deferred()
- reactor.callLater(3, result.callback, {'total': total})
- return result
- Sum.responder(slowSum)
-
- This is transparent to the caller.
-
- You can also define the propagation of specific errors in AMP. For example,
- for the slightly more complicated case of division, we might have to deal with
- division by zero::
-
- class Divide(amp.Command):
- arguments = [('numerator', amp.Integer()),
- ('denominator', amp.Integer())]
- response = [('result', amp.Float())]
- errors = {ZeroDivisionError: 'ZERO_DIVISION'}
-
- The 'errors' mapping here tells AMP that if a responder to Divide emits a
- L{ZeroDivisionError}, then the other side should be informed that an error of
- the type 'ZERO_DIVISION' has occurred. Writing a responder which takes
- advantage of this is very simple - just raise your exception normally::
-
- class JustDivide(amp.AMP):
- def divide(self, numerator, denominator):
- result = numerator / denominator
- print 'Divided: %d / %d = %d' % (numerator, denominator, total)
- return {'result': result}
- Divide.responder(divide)
-
- On the client side, the errors mapping will be used to determine what the
- 'ZERO_DIVISION' error means, and translated into an asynchronous exception,
- which can be handled normally as any L{Deferred} would be::
-
- def trapZero(result):
- result.trap(ZeroDivisionError)
- print "Divided by zero: returning INF"
- return 1e1000
- ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
- lambda p: p.callRemote(Divide, numerator=1234,
- denominator=0)
- ).addErrback(trapZero)
-
- For a complete, runnable example of both of these commands, see the files in
- the Twisted repository::
-
- doc/core/examples/ampserver.py
- doc/core/examples/ampclient.py
-
- On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and
- values, and empty keys to separate messages::
-
- <2-byte length><key><2-byte length><value>
- <2-byte length><key><2-byte length><value>
- ...
- <2-byte length><key><2-byte length><value>
- <NUL><NUL> # Empty Key == End of Message
-
- And so on. Because it's tedious to refer to lengths and NULs constantly, the
- documentation will refer to packets as if they were newline delimited, like
- so::
-
- C: _command: sum
- C: _ask: ef639e5c892ccb54
- C: a: 13
- C: b: 81
-
- S: _answer: ef639e5c892ccb54
- S: total: 94
-
- Notes:
-
- In general, the order of keys is arbitrary. Specific uses of AMP may impose an
- ordering requirement, but unless this is specified explicitly, any ordering may
- be generated and any ordering must be accepted. This applies to the
- command-related keys I{_command} and I{_ask} as well as any other keys.
-
- Values are limited to the maximum encodable size in a 16-bit length, 65535
- bytes.
-
- Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes.
- Note that we still use 2-byte lengths to encode keys. This small redundancy
- has several features:
-
- - If an implementation becomes confused and starts emitting corrupt data,
- or gets keys confused with values, many common errors will be signalled
- immediately instead of delivering obviously corrupt packets.
-
- - A single NUL will separate every key, and a double NUL separates
- messages. This provides some redundancy when debugging traffic dumps.
-
- - NULs will be present at regular intervals along the protocol, providing
- some padding for otherwise braindead C implementations of the protocol,
- so that <stdio.h> string functions will see the NUL and stop.
-
- - This makes it possible to run an AMP server on a port also used by a
- plain-text protocol, and easily distinguish between non-AMP clients (like
- web browsers) which issue non-NUL as the first byte, and AMP clients,
- which always issue NUL as the first byte.
-
- @var MAX_VALUE_LENGTH: The maximum length of a message.
- @type MAX_VALUE_LENGTH: L{int}
-
- @var ASK: Marker for an Ask packet.
- @type ASK: L{bytes}
-
- @var ANSWER: Marker for an Answer packet.
- @type ANSWER: L{bytes}
-
- @var COMMAND: Marker for a Command packet.
- @type COMMAND: L{bytes}
-
- @var ERROR: Marker for an AMP box of error type.
- @type ERROR: L{bytes}
-
- @var ERROR_CODE: Marker for an AMP box containing the code of an error.
- @type ERROR_CODE: L{bytes}
-
- @var ERROR_DESCRIPTION: Marker for an AMP box containing the description of the
- error.
- @type ERROR_DESCRIPTION: L{bytes}
- """
-
-
- import datetime
- import decimal
- import warnings
- from functools import partial
- from io import BytesIO
- from itertools import count
- from struct import pack
- from types import MethodType
- from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
-
- from zope.interface import Interface, implementer
-
- from twisted.internet.defer import Deferred, fail, maybeDeferred
- from twisted.internet.error import ConnectionClosed, ConnectionLost, PeerVerifyError
- from twisted.internet.interfaces import IFileDescriptorReceiver
- from twisted.internet.main import CONNECTION_LOST
- from twisted.internet.protocol import Protocol
- from twisted.protocols.basic import Int16StringReceiver, StatefulStringProtocol
- from twisted.python import filepath, log
- from twisted.python._tzhelper import (
- UTC as utc,
- FixedOffsetTimeZone as _FixedOffsetTZInfo,
- )
- from twisted.python.compat import nativeString
- from twisted.python.failure import Failure
- from twisted.python.reflect import accumulateClassDict
-
- try:
- from twisted.internet import ssl as _ssl
-
- if _ssl.supported:
- from twisted.internet.ssl import DN, Certificate, CertificateOptions, KeyPair
- else:
- ssl = None
- except ImportError:
- ssl = None
- else:
- ssl = _ssl
-
-
- __all__ = [
- "AMP",
- "ANSWER",
- "ASK",
- "AmpBox",
- "AmpError",
- "AmpList",
- "Argument",
- "BadLocalReturn",
- "BinaryBoxProtocol",
- "Boolean",
- "Box",
- "BoxDispatcher",
- "COMMAND",
- "Command",
- "CommandLocator",
- "Decimal",
- "Descriptor",
- "ERROR",
- "ERROR_CODE",
- "ERROR_DESCRIPTION",
- "Float",
- "IArgumentType",
- "IBoxReceiver",
- "IBoxSender",
- "IResponderLocator",
- "IncompatibleVersions",
- "Integer",
- "InvalidSignature",
- "ListOf",
- "MAX_KEY_LENGTH",
- "MAX_VALUE_LENGTH",
- "MalformedAmpBox",
- "NoEmptyBoxes",
- "OnlyOneTLS",
- "PROTOCOL_ERRORS",
- "PYTHON_KEYWORDS",
- "Path",
- "ProtocolSwitchCommand",
- "ProtocolSwitched",
- "QuitBox",
- "RemoteAmpError",
- "SimpleStringLocator",
- "StartTLS",
- "String",
- "TooLong",
- "UNHANDLED_ERROR_CODE",
- "UNKNOWN_ERROR_CODE",
- "UnhandledCommand",
- "utc",
- "Unicode",
- "UnknownRemoteError",
- "parse",
- "parseString",
- ]
-
-
- ASK = b"_ask"
- ANSWER = b"_answer"
- COMMAND = b"_command"
- ERROR = b"_error"
- ERROR_CODE = b"_error_code"
- ERROR_DESCRIPTION = b"_error_description"
- UNKNOWN_ERROR_CODE = b"UNKNOWN"
- UNHANDLED_ERROR_CODE = b"UNHANDLED"
-
- MAX_KEY_LENGTH = 0xFF
- MAX_VALUE_LENGTH = 0xFFFF
-
-
- class IArgumentType(Interface):
- """
- An L{IArgumentType} can serialize a Python object into an AMP box and
- deserialize information from an AMP box back into a Python object.
-
- @since: 9.0
- """
-
- def fromBox(name, strings, objects, proto):
- """
- Given an argument name and an AMP box containing serialized values,
- extract one or more Python objects and add them to the C{objects}
- dictionary.
-
- @param name: The name associated with this argument. Most commonly
- this is the key which can be used to find a serialized value in
- C{strings}.
- @type name: C{bytes}
-
- @param strings: The AMP box from which to extract one or more
- values.
- @type strings: C{dict}
-
- @param objects: The output dictionary to populate with the value for
- this argument. The key used will be derived from C{name}. It may
- differ; in Python 3, for example, the key will be a Unicode/native
- string. See L{_wireNameToPythonIdentifier}.
- @type objects: C{dict}
-
- @param proto: The protocol instance which received the AMP box being
- interpreted. Most likely this is an instance of L{AMP}, but
- this is not guaranteed.
-
- @return: L{None}
- """
-
- def toBox(name, strings, objects, proto):
- """
- Given an argument name and a dictionary containing structured Python
- objects, serialize values into one or more strings and add them to
- the C{strings} dictionary.
-
- @param name: The name associated with this argument. Most commonly
- this is the key in C{strings} to associate with a C{bytes} giving
- the serialized form of that object.
- @type name: C{bytes}
-
- @param strings: The AMP box into which to insert one or more strings.
- @type strings: C{dict}
-
- @param objects: The input dictionary from which to extract Python
- objects to serialize. The key used will be derived from C{name}.
- It may differ; in Python 3, for example, the key will be a
- Unicode/native string. See L{_wireNameToPythonIdentifier}.
- @type objects: C{dict}
-
- @param proto: The protocol instance which will send the AMP box once
- it is fully populated. Most likely this is an instance of
- L{AMP}, but this is not guaranteed.
-
- @return: L{None}
- """
-
-
- class IBoxSender(Interface):
- """
- A transport which can send L{AmpBox} objects.
- """
-
- def sendBox(box):
- """
- Send an L{AmpBox}.
-
- @raise ProtocolSwitched: if the underlying protocol has been
- switched.
-
- @raise ConnectionLost: if the underlying connection has already been
- lost.
- """
-
- def unhandledError(failure):
- """
- An unhandled error occurred in response to a box. Log it
- appropriately.
-
- @param failure: a L{Failure} describing the error that occurred.
- """
-
-
- class IBoxReceiver(Interface):
- """
- An application object which can receive L{AmpBox} objects and dispatch them
- appropriately.
- """
-
- def startReceivingBoxes(boxSender):
- """
- The L{IBoxReceiver.ampBoxReceived} method will start being called;
- boxes may be responded to by responding to the given L{IBoxSender}.
-
- @param boxSender: an L{IBoxSender} provider.
- """
-
- def ampBoxReceived(box):
- """
- A box was received from the transport; dispatch it appropriately.
- """
-
- def stopReceivingBoxes(reason):
- """
- No further boxes will be received on this connection.
-
- @type reason: L{Failure}
- """
-
-
- class IResponderLocator(Interface):
- """
- An application object which can look up appropriate responder methods for
- AMP commands.
- """
-
- def locateResponder(name):
- """
- Locate a responder method appropriate for the named command.
-
- @param name: the wire-level name (commandName) of the AMP command to be
- responded to.
- @type name: C{bytes}
-
- @return: a 1-argument callable that takes an L{AmpBox} with argument
- values for the given command, and returns an L{AmpBox} containing
- argument values for the named command, or a L{Deferred} that fires the
- same.
- """
-
-
- class AmpError(Exception):
- """
- Base class of all Amp-related exceptions.
- """
-
-
- class ProtocolSwitched(Exception):
- """
- Connections which have been switched to other protocols can no longer
- accept traffic at the AMP level. This is raised when you try to send it.
- """
-
-
- class OnlyOneTLS(AmpError):
- """
- This is an implementation limitation; TLS may only be started once per
- connection.
- """
-
-
- class NoEmptyBoxes(AmpError):
- """
- You can't have empty boxes on the connection. This is raised when you
- receive or attempt to send one.
- """
-
-
- class InvalidSignature(AmpError):
- """
- You didn't pass all the required arguments.
- """
-
-
- class TooLong(AmpError):
- """
- One of the protocol's length limitations was violated.
-
- @ivar isKey: true if the string being encoded in a key position, false if
- it was in a value position.
-
- @ivar isLocal: Was the string encoded locally, or received too long from
- the network? (It's only physically possible to encode "too long" values on
- the network for keys.)
-
- @ivar value: The string that was too long.
-
- @ivar keyName: If the string being encoded was in a value position, what
- key was it being encoded for?
- """
-
- def __init__(self, isKey, isLocal, value, keyName=None):
- AmpError.__init__(self)
- self.isKey = isKey
- self.isLocal = isLocal
- self.value = value
- self.keyName = keyName
-
- def __repr__(self) -> str:
- hdr = self.isKey and "key" or "value"
- if not self.isKey:
- hdr += " " + repr(self.keyName)
- lcl = self.isLocal and "local" or "remote"
- return "%s %s too long: %d" % (lcl, hdr, len(self.value))
-
-
- class BadLocalReturn(AmpError):
- """
- A bad value was returned from a local command; we were unable to coerce it.
- """
-
- def __init__(self, message: str, enclosed: Failure) -> None:
- AmpError.__init__(self)
- self.message = message
- self.enclosed = enclosed
-
- def __repr__(self) -> str:
- return self.message + " " + self.enclosed.getBriefTraceback()
-
- __str__ = __repr__
-
-
- class RemoteAmpError(AmpError):
- """
- This error indicates that something went wrong on the remote end of the
- connection, and the error was serialized and transmitted to you.
- """
-
- def __init__(self, errorCode, description, fatal=False, local=None):
- """Create a remote error with an error code and description.
-
- @param errorCode: the AMP error code of this error.
- @type errorCode: C{bytes}
-
- @param description: some text to show to the user.
- @type description: C{str}
-
- @param fatal: a boolean, true if this error should terminate the
- connection.
-
- @param local: a local Failure, if one exists.
- """
- if local:
- localwhat = " (local)"
- othertb = local.getBriefTraceback()
- else:
- localwhat = ""
- othertb = ""
-
- # Backslash-escape errorCode. Python 3.5 can do this natively
- # ("backslashescape") but Python 2.7 and Python 3.4 can't.
- errorCodeForMessage = "".join(
- f"\\x{c:2x}" if c >= 0x80 else chr(c) for c in errorCode
- )
-
- if othertb:
- message = "Code<{}>{}: {}\n{}".format(
- errorCodeForMessage,
- localwhat,
- description,
- othertb,
- )
- else:
- message = "Code<{}>{}: {}".format(
- errorCodeForMessage, localwhat, description
- )
-
- super().__init__(message)
- self.local = local
- self.errorCode = errorCode
- self.description = description
- self.fatal = fatal
-
-
- class UnknownRemoteError(RemoteAmpError):
- """
- This means that an error whose type we can't identify was raised from the
- other side.
- """
-
- def __init__(self, description):
- errorCode = UNKNOWN_ERROR_CODE
- RemoteAmpError.__init__(self, errorCode, description)
-
-
- class MalformedAmpBox(AmpError):
- """
- This error indicates that the wire-level protocol was malformed.
- """
-
-
- class UnhandledCommand(AmpError):
- """
- A command received via amp could not be dispatched.
- """
-
-
- class IncompatibleVersions(AmpError):
- """
- It was impossible to negotiate a compatible version of the protocol with
- the other end of the connection.
- """
-
-
- PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand}
-
-
- class AmpBox(dict):
- """
- I am a packet in the AMP protocol, much like a
- regular bytes:bytes dictionary.
- """
-
- # be like a regular dictionary don't magically
- # acquire a __dict__...
- __slots__: List[str] = []
-
- def __init__(self, *args, **kw):
- """
- Initialize a new L{AmpBox}.
-
- In Python 3, keyword arguments MUST be Unicode/native strings whereas
- in Python 2 they could be either byte strings or Unicode strings.
-
- However, all keys of an L{AmpBox} MUST be byte strings, or possible to
- transparently coerce into byte strings (i.e. Python 2).
-
- In Python 3, therefore, native string keys are coerced to byte strings
- by encoding as ASCII. This can result in C{UnicodeEncodeError} being
- raised.
-
- @param args: See C{dict}, but all keys and values should be C{bytes}.
- On Python 3, native strings may be used as keys provided they
- contain only ASCII characters.
-
- @param kw: See C{dict}, but all keys and values should be C{bytes}.
- On Python 3, native strings may be used as keys provided they
- contain only ASCII characters.
-
- @raise UnicodeEncodeError: When a native string key cannot be coerced
- to an ASCII byte string (Python 3 only).
- """
- super().__init__(*args, **kw)
- nonByteNames = [n for n in self if not isinstance(n, bytes)]
- for nonByteName in nonByteNames:
- byteName = nonByteName.encode("ascii")
- self[byteName] = self.pop(nonByteName)
-
- def copy(self):
- """
- Return another AmpBox just like me.
- """
- newBox = self.__class__()
- newBox.update(self)
- return newBox
-
- def serialize(self):
- """
- Convert me into a wire-encoded string.
-
- @return: a C{bytes} encoded according to the rules described in the
- module docstring.
- """
- i = sorted(self.items())
- L = []
- w = L.append
- for k, v in i:
- if type(k) == str:
- raise TypeError("Unicode key not allowed: %r" % k)
- if type(v) == str:
- raise TypeError(f"Unicode value for key {k!r} not allowed: {v!r}")
- if len(k) > MAX_KEY_LENGTH:
- raise TooLong(True, True, k, None)
- if len(v) > MAX_VALUE_LENGTH:
- raise TooLong(False, True, v, k)
- for kv in k, v:
- w(pack("!H", len(kv)))
- w(kv)
- w(pack("!H", 0))
- return b"".join(L)
-
- def _sendTo(self, proto):
- """
- Serialize and send this box to an Amp instance. By the time it is being
- sent, several keys are required. I must have exactly ONE of::
-
- _ask
- _answer
- _error
-
- If the '_ask' key is set, then the '_command' key must also be
- set.
-
- @param proto: an AMP instance.
- """
- proto.sendBox(self)
-
- def __repr__(self) -> str:
- return f"AmpBox({dict.__repr__(self)})"
-
-
- # amp.Box => AmpBox
-
- Box = AmpBox
-
-
- class QuitBox(AmpBox):
- """
- I am an AmpBox that, upon being sent, terminates the connection.
- """
-
- __slots__: List[str] = []
-
- def __repr__(self) -> str:
- return f"QuitBox(**{super().__repr__()})"
-
- def _sendTo(self, proto):
- """
- Immediately call loseConnection after sending.
- """
- super()._sendTo(proto)
- proto.transport.loseConnection()
-
-
- class _SwitchBox(AmpBox):
- """
- Implementation detail of ProtocolSwitchCommand: I am an AmpBox which sets
- up state for the protocol to switch.
- """
-
- # DON'T set __slots__ here; we do have an attribute.
-
- def __init__(self, innerProto, **kw):
- """
- Create a _SwitchBox with the protocol to switch to after being sent.
-
- @param innerProto: the protocol instance to switch to.
- @type innerProto: an IProtocol provider.
- """
- super().__init__(**kw)
- self.innerProto = innerProto
-
- def __repr__(self) -> str:
- return "_SwitchBox({!r}, **{})".format(
- self.innerProto,
- dict.__repr__(self),
- )
-
- def _sendTo(self, proto):
- """
- Send me; I am the last box on the connection. All further traffic will be
- over the new protocol.
- """
- super()._sendTo(proto)
- proto._lockForSwitch()
- proto._switchTo(self.innerProto)
-
-
- @implementer(IBoxReceiver)
- class BoxDispatcher:
- """
- A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es,
- both incoming and outgoing, to their appropriate destinations.
-
- Outgoing commands are converted into L{Deferred}s and outgoing boxes, and
- associated tracking state to fire those L{Deferred} when '_answer' boxes
- come back. Incoming '_answer' and '_error' boxes are converted into
- callbacks and errbacks on those L{Deferred}s, respectively.
-
- Incoming '_ask' boxes are converted into method calls on a supplied method
- locator.
-
- @ivar _outstandingRequests: a dictionary mapping request IDs to
- L{Deferred}s which were returned for those requests.
-
- @ivar locator: an object with a L{CommandLocator.locateResponder} method
- that locates a responder function that takes a Box and returns a result
- (either a Box or a Deferred which fires one).
-
- @ivar boxSender: an object which can send boxes, via the L{_sendBoxCommand}
- method, such as an L{AMP} instance.
- @type boxSender: L{IBoxSender}
- """
-
- _failAllReason = None
- _outstandingRequests = None
- _counter = 0
- boxSender = None
-
- def __init__(self, locator):
- self._outstandingRequests = {}
- self.locator = locator
-
- def startReceivingBoxes(self, boxSender):
- """
- The given boxSender is going to start calling boxReceived on this
- L{BoxDispatcher}.
-
- @param boxSender: The L{IBoxSender} to send command responses to.
- """
- self.boxSender = boxSender
-
- def stopReceivingBoxes(self, reason):
- """
- No further boxes will be received here. Terminate all currently
- outstanding command deferreds with the given reason.
- """
- self.failAllOutgoing(reason)
-
- def failAllOutgoing(self, reason):
- """
- Call the errback on all outstanding requests awaiting responses.
-
- @param reason: the Failure instance to pass to those errbacks.
- """
- self._failAllReason = reason
- OR = self._outstandingRequests.items()
- self._outstandingRequests = None # we can never send another request
- for key, value in OR:
- value.errback(reason)
-
- def _nextTag(self):
- """
- Generate protocol-local serial numbers for _ask keys.
-
- @return: a string that has not yet been used on this connection.
- """
- self._counter += 1
- return b"%x" % (self._counter,)
-
- def _sendBoxCommand(self, command, box, requiresAnswer=True):
- """
- Send a command across the wire with the given C{amp.Box}.
-
- Mutate the given box to give it any additional keys (_command, _ask)
- required for the command and request/response machinery, then send it.
-
- If requiresAnswer is True, returns a C{Deferred} which fires when a
- response is received. The C{Deferred} is fired with an C{amp.Box} on
- success, or with an C{amp.RemoteAmpError} if an error is received.
-
- If the Deferred fails and the error is not handled by the caller of
- this method, the failure will be logged and the connection dropped.
-
- @param command: a C{bytes}, the name of the command to issue.
-
- @param box: an AmpBox with the arguments for the command.
-
- @param requiresAnswer: a boolean. Defaults to True. If True, return a
- Deferred which will fire when the other side responds to this command.
- If False, return None and do not ask the other side for acknowledgement.
-
- @return: a Deferred which fires the AmpBox that holds the response to
- this command, or None, as specified by requiresAnswer.
-
- @raise ProtocolSwitched: if the protocol has been switched.
- """
- if self._failAllReason is not None:
- if requiresAnswer:
- return fail(self._failAllReason)
- else:
- return None
- box[COMMAND] = command
- tag = self._nextTag()
- if requiresAnswer:
- box[ASK] = tag
- box._sendTo(self.boxSender)
- if requiresAnswer:
- result = self._outstandingRequests[tag] = Deferred()
- else:
- result = None
- return result
-
- def callRemoteString(self, command, requiresAnswer=True, **kw):
- """
- This is a low-level API, designed only for optimizing simple messages
- for which the overhead of parsing is too great.
-
- @param command: a C{bytes} naming the command.
-
- @param kw: arguments to the amp box.
-
- @param requiresAnswer: a boolean. Defaults to True. If True, return a
- Deferred which will fire when the other side responds to this command.
- If False, return None and do not ask the other side for acknowledgement.
-
- @return: a Deferred which fires the AmpBox that holds the response to
- this command, or None, as specified by requiresAnswer.
- """
- box = Box(kw)
- return self._sendBoxCommand(command, box, requiresAnswer)
-
- def callRemote(self, commandType, *a, **kw):
- """
- This is the primary high-level API for sending messages via AMP. Invoke it
- with a command and appropriate arguments to send a message to this
- connection's peer.
-
- @param commandType: a subclass of Command.
- @type commandType: L{type}
-
- @param a: Positional (special) parameters taken by the command.
- Positional parameters will typically not be sent over the wire. The
- only command included with AMP which uses positional parameters is
- L{ProtocolSwitchCommand}, which takes the protocol that will be
- switched to as its first argument.
-
- @param kw: Keyword arguments taken by the command. These are the
- arguments declared in the command's 'arguments' attribute. They will
- be encoded and sent to the peer as arguments for the L{commandType}.
-
- @return: If L{commandType} has a C{requiresAnswer} attribute set to
- L{False}, then return L{None}. Otherwise, return a L{Deferred} which
- fires with a dictionary of objects representing the result of this
- call. Additionally, this L{Deferred} may fail with an exception
- representing a connection failure, with L{UnknownRemoteError} if the
- other end of the connection fails for an unknown reason, or with any
- error specified as a key in L{commandType}'s C{errors} dictionary.
- """
-
- # XXX this takes command subclasses and not command objects on purpose.
- # There's really no reason to have all this back-and-forth between
- # command objects and the protocol, and the extra object being created
- # (the Command instance) is pointless. Command is kind of like
- # Interface, and should be more like it.
-
- # In other words, the fact that commandType is instantiated here is an
- # implementation detail. Don't rely on it.
-
- try:
- co = commandType(*a, **kw)
- except BaseException:
- return fail()
- return co._doCommand(self)
-
- def unhandledError(self, failure):
- """
- This is a terminal callback called after application code has had a
- chance to quash any errors.
- """
- return self.boxSender.unhandledError(failure)
-
- def _answerReceived(self, box):
- """
- An AMP box was received that answered a command previously sent with
- L{callRemote}.
-
- @param box: an AmpBox with a value for its L{ANSWER} key.
- """
- question = self._outstandingRequests.pop(box[ANSWER])
- question.addErrback(self.unhandledError)
- question.callback(box)
-
- def _errorReceived(self, box):
- """
- An AMP box was received that answered a command previously sent with
- L{callRemote}, with an error.
-
- @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE},
- and L{ERROR_DESCRIPTION} keys.
- """
- question = self._outstandingRequests.pop(box[ERROR])
- question.addErrback(self.unhandledError)
- errorCode = box[ERROR_CODE]
- description = box[ERROR_DESCRIPTION]
- if isinstance(description, bytes):
- description = description.decode("utf-8", "replace")
- if errorCode in PROTOCOL_ERRORS:
- exc = PROTOCOL_ERRORS[errorCode](errorCode, description)
- else:
- exc = RemoteAmpError(errorCode, description)
- question.errback(Failure(exc))
-
- def _commandReceived(self, box):
- """
- @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK}
- keys.
- """
-
- def formatAnswer(answerBox):
- answerBox[ANSWER] = box[ASK]
- return answerBox
-
- def formatError(error):
- if error.check(RemoteAmpError):
- code = error.value.errorCode
- desc = error.value.description
- if isinstance(desc, str):
- desc = desc.encode("utf-8", "replace")
- if error.value.fatal:
- errorBox = QuitBox()
- else:
- errorBox = AmpBox()
- else:
- errorBox = QuitBox()
- log.err(error) # here is where server-side logging happens
- # if the error isn't handled
- code = UNKNOWN_ERROR_CODE
- desc = b"Unknown Error"
- errorBox[ERROR] = box[ASK]
- errorBox[ERROR_DESCRIPTION] = desc
- errorBox[ERROR_CODE] = code
- return errorBox
-
- deferred = self.dispatchCommand(box)
- if ASK in box:
- deferred.addCallbacks(formatAnswer, formatError)
- deferred.addCallback(self._safeEmit)
- deferred.addErrback(self.unhandledError)
-
- def ampBoxReceived(self, box):
- """
- An AmpBox was received, representing a command, or an answer to a
- previously issued command (either successful or erroneous). Respond to
- it according to its contents.
-
- @param box: an AmpBox
-
- @raise NoEmptyBoxes: when a box is received that does not contain an
- '_answer', '_command' / '_ask', or '_error' key; i.e. one which does not
- fit into the command / response protocol defined by AMP.
- """
- if ANSWER in box:
- self._answerReceived(box)
- elif ERROR in box:
- self._errorReceived(box)
- elif COMMAND in box:
- self._commandReceived(box)
- else:
- raise NoEmptyBoxes(box)
-
- def _safeEmit(self, aBox):
- """
- Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors
- which cannot be usefully handled.
- """
- try:
- aBox._sendTo(self.boxSender)
- except (ProtocolSwitched, ConnectionLost):
- pass
-
- def dispatchCommand(self, box):
- """
- A box with a _command key was received.
-
- Dispatch it to a local handler call it.
-
- @param box: an AmpBox to be dispatched.
- """
- cmd = box[COMMAND]
- responder = self.locator.locateResponder(cmd)
- if responder is None:
- description = f"Unhandled Command: {cmd!r}"
- return fail(
- RemoteAmpError(
- UNHANDLED_ERROR_CODE,
- description,
- False,
- local=Failure(UnhandledCommand()),
- )
- )
- return maybeDeferred(responder, box)
-
-
- class _CommandLocatorMeta(type):
- """
- This metaclass keeps track of all of the Command.responder-decorated
- methods defined since the last CommandLocator subclass was defined. It
- assumes (usually correctly, but unfortunately not necessarily so) that
- those commands responders were all declared as methods of the class
- being defined. Note that this list can be incorrect if users use the
- Command.responder decorator outside the context of a CommandLocator
- class declaration.
-
- Command responders defined on subclasses are given precedence over
- those inherited from a base class.
-
- The Command.responder decorator explicitly cooperates with this
- metaclass.
- """
-
- _currentClassCommands: "List[Tuple[Command, Callable]]" = []
-
- def __new__(cls, name, bases, attrs):
- commands = cls._currentClassCommands[:]
- cls._currentClassCommands[:] = []
- cd = attrs["_commandDispatch"] = {}
- subcls = type.__new__(cls, name, bases, attrs)
- ancestors = list(subcls.__mro__[1:])
- ancestors.reverse()
- for ancestor in ancestors:
- cd.update(getattr(ancestor, "_commandDispatch", {}))
- for commandClass, responderFunc in commands:
- cd[commandClass.commandName] = (commandClass, responderFunc)
- if bases and (subcls.lookupFunction != CommandLocator.lookupFunction):
-
- def locateResponder(self, name):
- warnings.warn(
- "Override locateResponder, not lookupFunction.",
- category=PendingDeprecationWarning,
- stacklevel=2,
- )
- return self.lookupFunction(name)
-
- subcls.locateResponder = locateResponder
- return subcls
-
-
- @implementer(IResponderLocator)
- class CommandLocator(metaclass=_CommandLocatorMeta):
- """
- A L{CommandLocator} is a collection of responders to AMP L{Command}s, with
- the help of the L{Command.responder} decorator.
- """
-
- def _wrapWithSerialization(self, aCallable, command):
- """
- Wrap aCallable with its command's argument de-serialization
- and result serialization logic.
-
- @param aCallable: a callable with a 'command' attribute, designed to be
- called with keyword arguments.
-
- @param command: the command class whose serialization to use.
-
- @return: a 1-arg callable which, when invoked with an AmpBox, will
- deserialize the argument list and invoke appropriate user code for the
- callable's command, returning a Deferred which fires with the result or
- fails with an error.
- """
-
- def doit(box):
- kw = command.parseArguments(box, self)
-
- def checkKnownErrors(error):
- key = error.trap(*command.allErrors)
- code = command.allErrors[key]
- desc = str(error.value)
- return Failure(
- RemoteAmpError(code, desc, key in command.fatalErrors, local=error)
- )
-
- def makeResponseFor(objects):
- try:
- return command.makeResponse(objects, self)
- except BaseException:
- # let's helpfully log this.
- originalFailure = Failure()
- raise BadLocalReturn(
- "%r returned %r and %r could not serialize it"
- % (aCallable, objects, command),
- originalFailure,
- )
-
- return (
- maybeDeferred(aCallable, **kw)
- .addCallback(makeResponseFor)
- .addErrback(checkKnownErrors)
- )
-
- return doit
-
- def lookupFunction(self, name):
- """
- Deprecated synonym for L{CommandLocator.locateResponder}
- """
- if self.__class__.lookupFunction != CommandLocator.lookupFunction:
- return CommandLocator.locateResponder(self, name)
- else:
- warnings.warn(
- "Call locateResponder, not lookupFunction.",
- category=PendingDeprecationWarning,
- stacklevel=2,
- )
- return self.locateResponder(name)
-
- def locateResponder(self, name):
- """
- Locate a callable to invoke when executing the named command.
-
- @param name: the normalized name (from the wire) of the command.
- @type name: C{bytes}
-
- @return: a 1-argument function that takes a Box and returns a box or a
- Deferred which fires a Box, for handling the command identified by the
- given name, or None, if no appropriate responder can be found.
- """
- # Try to find a high-level method to invoke, and if we can't find one,
- # fall back to a low-level one.
- cd = self._commandDispatch
- if name in cd:
- commandClass, responderFunc = cd[name]
- responderMethod = MethodType(responderFunc, self)
- return self._wrapWithSerialization(responderMethod, commandClass)
-
-
- @implementer(IResponderLocator)
- class SimpleStringLocator:
- """
- Implement the L{AMP.locateResponder} method to do simple, string-based
- dispatch.
- """
-
- baseDispatchPrefix = b"amp_"
-
- def locateResponder(self, name):
- """
- Locate a callable to invoke when executing the named command.
-
- @return: a function with the name C{"amp_" + name} on the same
- instance, or None if no such function exists.
- This function will then be called with the L{AmpBox} itself as an
- argument.
-
- @param name: the normalized name (from the wire) of the command.
- @type name: C{bytes}
- """
- fName = nativeString(self.baseDispatchPrefix + name.upper())
- return getattr(self, fName, None)
-
-
- PYTHON_KEYWORDS = [
- "and",
- "del",
- "for",
- "is",
- "raise",
- "assert",
- "elif",
- "from",
- "lambda",
- "return",
- "break",
- "else",
- "global",
- "not",
- "try",
- "class",
- "except",
- "if",
- "or",
- "while",
- "continue",
- "exec",
- "import",
- "pass",
- "yield",
- "def",
- "finally",
- "in",
- "print",
- ]
-
-
- def _wireNameToPythonIdentifier(key):
- """
- (Private) Normalize an argument name from the wire for use with Python
- code. If the return value is going to be a python keyword it will be
- capitalized. If it contains any dashes they will be replaced with
- underscores.
-
- The rationale behind this method is that AMP should be an inherently
- multi-language protocol, so message keys may contain all manner of bizarre
- bytes. This is not a complete solution; there are still forms of arguments
- that this implementation will be unable to parse. However, Python
- identifiers share a huge raft of properties with identifiers from many
- other languages, so this is a 'good enough' effort for now. We deal
- explicitly with dashes because that is the most likely departure: Lisps
- commonly use dashes to separate method names, so protocols initially
- implemented in a lisp amp dialect may use dashes in argument or command
- names.
-
- @param key: a C{bytes}, looking something like 'foo-bar-baz' or 'from'
- @type key: C{bytes}
-
- @return: a native string which is a valid python identifier, looking
- something like 'foo_bar_baz' or 'From'.
- """
- lkey = nativeString(key.replace(b"-", b"_"))
- if lkey in PYTHON_KEYWORDS:
- return lkey.title()
- return lkey
-
-
- @implementer(IArgumentType)
- class Argument:
- """
- Base-class of all objects that take values from Amp packets and convert
- them into objects for Python functions.
-
- This implementation of L{IArgumentType} provides several higher-level
- hooks for subclasses to override. See L{toString} and L{fromString}
- which will be used to define the behavior of L{IArgumentType.toBox} and
- L{IArgumentType.fromBox}, respectively.
- """
-
- optional = False
-
- def __init__(self, optional=False):
- """
- Create an Argument.
-
- @param optional: a boolean indicating whether this argument can be
- omitted in the protocol.
- """
- self.optional = optional
-
- def retrieve(self, d, name, proto):
- """
- Retrieve the given key from the given dictionary, removing it if found.
-
- @param d: a dictionary.
-
- @param name: a key in I{d}.
-
- @param proto: an instance of an AMP.
-
- @raise KeyError: if I am not optional and no value was found.
-
- @return: d[name].
- """
- if self.optional:
- value = d.get(name)
- if value is not None:
- del d[name]
- else:
- value = d.pop(name)
- return value
-
- def fromBox(self, name, strings, objects, proto):
- """
- Populate an 'out' dictionary with mapping names to Python values
- decoded from an 'in' AmpBox mapping strings to string values.
-
- @param name: the argument name to retrieve
- @type name: C{bytes}
-
- @param strings: The AmpBox to read string(s) from, a mapping of
- argument names to string values.
- @type strings: AmpBox
-
- @param objects: The dictionary to write object(s) to, a mapping of
- names to Python objects. Keys will be native strings.
- @type objects: dict
-
- @param proto: an AMP instance.
- """
- st = self.retrieve(strings, name, proto)
- nk = _wireNameToPythonIdentifier(name)
- if self.optional and st is None:
- objects[nk] = None
- else:
- objects[nk] = self.fromStringProto(st, proto)
-
- def toBox(self, name, strings, objects, proto):
- """
- Populate an 'out' AmpBox with strings encoded from an 'in' dictionary
- mapping names to Python values.
-
- @param name: the argument name to retrieve
- @type name: C{bytes}
-
- @param strings: The AmpBox to write string(s) to, a mapping of
- argument names to string values.
- @type strings: AmpBox
-
- @param objects: The dictionary to read object(s) from, a mapping of
- names to Python objects. Keys should be native strings.
-
- @type objects: dict
-
- @param proto: the protocol we are converting for.
- @type proto: AMP
- """
- obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto)
- if self.optional and obj is None:
- # strings[name] = None
- pass
- else:
- strings[name] = self.toStringProto(obj, proto)
-
- def fromStringProto(self, inString, proto):
- """
- Convert a string to a Python value.
-
- @param inString: the string to convert.
- @type inString: C{bytes}
-
- @param proto: the protocol we are converting for.
- @type proto: AMP
-
- @return: a Python object.
- """
- return self.fromString(inString)
-
- def toStringProto(self, inObject, proto):
- """
- Convert a Python object to a string.
-
- @param inObject: the object to convert.
-
- @param proto: the protocol we are converting for.
- @type proto: AMP
- """
- return self.toString(inObject)
-
- def fromString(self, inString):
- """
- Convert a string to a Python object. Subclasses must implement this.
-
- @param inString: the string to convert.
- @type inString: C{bytes}
-
- @return: the decoded value from C{inString}
- """
-
- def toString(self, inObject):
- """
- Convert a Python object into a string for passing over the network.
-
- @param inObject: an object of the type that this Argument is intended
- to deal with.
-
- @return: the wire encoding of inObject
- @rtype: C{bytes}
- """
-
-
- class Integer(Argument):
- """
- Encode any integer values of any size on the wire as the string
- representation.
-
- Example: C{123} becomes C{"123"}
- """
-
- fromString = int
-
- def toString(self, inObject):
- return b"%d" % (inObject,)
-
-
- class String(Argument):
- """
- Don't do any conversion at all; just pass through 'str'.
- """
-
- def toString(self, inObject):
- return inObject
-
- def fromString(self, inString):
- return inString
-
-
- class Float(Argument):
- """
- Encode floating-point values on the wire as their repr.
- """
-
- fromString = float
-
- def toString(self, inString):
- if not isinstance(inString, float):
- raise ValueError(f"Bad float value {inString!r}")
- return str(inString).encode("ascii")
-
-
- class Boolean(Argument):
- """
- Encode True or False as "True" or "False" on the wire.
- """
-
- def fromString(self, inString):
- if inString == b"True":
- return True
- elif inString == b"False":
- return False
- else:
- raise TypeError(f"Bad boolean value: {inString!r}")
-
- def toString(self, inObject):
- if inObject:
- return b"True"
- else:
- return b"False"
-
-
- class Unicode(String):
- """
- Encode a unicode string on the wire as UTF-8.
- """
-
- def toString(self, inObject):
- return String.toString(self, inObject.encode("utf-8"))
-
- def fromString(self, inString):
- return String.fromString(self, inString).decode("utf-8")
-
-
- class Path(Unicode):
- """
- Encode and decode L{filepath.FilePath} instances as paths on the wire.
-
- This is really intended for use with subprocess communication tools:
- exchanging pathnames on different machines over a network is not generally
- meaningful, but neither is it disallowed; you can use this to communicate
- about NFS paths, for example.
- """
-
- def fromString(self, inString):
- return filepath.FilePath(Unicode.fromString(self, inString))
-
- def toString(self, inObject):
- return Unicode.toString(self, inObject.asTextMode().path)
-
-
- class ListOf(Argument):
- """
- Encode and decode lists of instances of a single other argument type.
-
- For example, if you want to pass::
-
- [3, 7, 9, 15]
-
- You can create an argument like this::
-
- ListOf(Integer())
-
- The serialized form of the entire list is subject to the limit imposed by
- L{MAX_VALUE_LENGTH}. List elements are represented as 16-bit length
- prefixed strings. The argument type passed to the L{ListOf} initializer is
- responsible for producing the serialized form of each element.
-
- @ivar elementType: The L{Argument} instance used to encode and decode list
- elements (note, not an arbitrary L{IArgumentType} implementation:
- arguments must be implemented using only the C{fromString} and
- C{toString} methods, not the C{fromBox} and C{toBox} methods).
-
- @param optional: a boolean indicating whether this argument can be
- omitted in the protocol.
-
- @since: 10.0
- """
-
- def __init__(self, elementType, optional=False):
- self.elementType = elementType
- Argument.__init__(self, optional)
-
- def fromString(self, inString):
- """
- Convert the serialized form of a list of instances of some type back
- into that list.
- """
- strings = []
- parser = Int16StringReceiver()
- parser.stringReceived = strings.append
- parser.dataReceived(inString)
- elementFromString = self.elementType.fromString
- return [elementFromString(string) for string in strings]
-
- def toString(self, inObject):
- """
- Serialize the given list of objects to a single string.
- """
- strings = []
- for obj in inObject:
- serialized = self.elementType.toString(obj)
- strings.append(pack("!H", len(serialized)))
- strings.append(serialized)
- return b"".join(strings)
-
-
- class AmpList(Argument):
- """
- Convert a list of dictionaries into a list of AMP boxes on the wire.
-
- For example, if you want to pass::
-
- [{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}]
-
- You might use an AmpList like this in your arguments or response list::
-
- AmpList([('a', Integer()),
- ('b', Unicode())])
- """
-
- def __init__(self, subargs, optional=False):
- """
- Create an AmpList.
-
- @param subargs: a list of 2-tuples of ('name', argument) describing the
- schema of the dictionaries in the sequence of amp boxes.
- @type subargs: A C{list} of (C{bytes}, L{Argument}) tuples.
-
- @param optional: a boolean indicating whether this argument can be
- omitted in the protocol.
- """
- assert all(isinstance(name, bytes) for name, _ in subargs), (
- "AmpList should be defined with a list of (name, argument) "
- "tuples where `name' is a byte string, got: %r" % (subargs,)
- )
- self.subargs = subargs
- Argument.__init__(self, optional)
-
- def fromStringProto(self, inString, proto):
- boxes = parseString(inString)
- values = [_stringsToObjects(box, self.subargs, proto) for box in boxes]
- return values
-
- def toStringProto(self, inObject, proto):
- return b"".join(
- [
- _objectsToStrings(objects, self.subargs, Box(), proto).serialize()
- for objects in inObject
- ]
- )
-
-
- class Descriptor(Integer):
- """
- Encode and decode file descriptors for exchange over a UNIX domain socket.
-
- This argument type requires an AMP connection set up over an
- L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>} provider (for
- example, the kind of connection created by
- L{IReactorUNIX.connectUNIX<twisted.internet.interfaces.IReactorUNIX.connectUNIX>}
- and L{UNIXClientEndpoint<twisted.internet.endpoints.UNIXClientEndpoint>}).
-
- There is no correspondence between the integer value of the file descriptor
- on the sending and receiving sides, therefore an alternate approach is taken
- to matching up received descriptors with particular L{Descriptor}
- parameters. The argument is encoded to an ordinal (unique per connection)
- for inclusion in the AMP command or response box. The descriptor itself is
- sent using
- L{IUNIXTransport.sendFileDescriptor<twisted.internet.interfaces.IUNIXTransport.sendFileDescriptor>}.
- The receiver uses the order in which file descriptors are received and the
- ordinal value to come up with the received copy of the descriptor.
- """
-
- def fromStringProto(self, inString, proto):
- """
- Take a unique identifier associated with a file descriptor which must
- have been received by now and use it to look up that descriptor in a
- dictionary where they are kept.
-
- @param inString: The base representation (as a byte string) of an
- ordinal indicating which file descriptor corresponds to this usage
- of this argument.
- @type inString: C{str}
-
- @param proto: The protocol used to receive this descriptor. This
- protocol must be connected via a transport providing
- L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
- @type proto: L{BinaryBoxProtocol}
-
- @return: The file descriptor represented by C{inString}.
- @rtype: C{int}
- """
- return proto._getDescriptor(int(inString))
-
- def toStringProto(self, inObject, proto):
- """
- Send C{inObject}, an integer file descriptor, over C{proto}'s connection
- and return a unique identifier which will allow the receiver to
- associate the file descriptor with this argument.
-
- @param inObject: A file descriptor to duplicate over an AMP connection
- as the value for this argument.
- @type inObject: C{int}
-
- @param proto: The protocol which will be used to send this descriptor.
- This protocol must be connected via a transport providing
- L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
-
- @return: A byte string which can be used by the receiver to reconstruct
- the file descriptor.
- @rtype: C{bytes}
- """
- identifier = proto._sendFileDescriptor(inObject)
- outString = Integer.toStringProto(self, identifier, proto)
- return outString
-
-
- class _CommandMeta(type):
- """
- Metaclass hack to establish reverse-mappings for 'errors' and
- 'fatalErrors' as class vars.
- """
-
- def __new__(cls, name, bases, attrs):
- reverseErrors = attrs["reverseErrors"] = {}
- er = attrs["allErrors"] = {}
- if "commandName" not in attrs:
- attrs["commandName"] = name.encode("ascii")
- newtype = type.__new__(cls, name, bases, attrs)
-
- if not isinstance(newtype.commandName, bytes):
- raise TypeError(
- "Command names must be byte strings, got: {!r}".format(
- newtype.commandName
- )
- )
- for name, _ in newtype.arguments:
- if not isinstance(name, bytes):
- raise TypeError(f"Argument names must be byte strings, got: {name!r}")
- for name, _ in newtype.response:
- if not isinstance(name, bytes):
- raise TypeError(f"Response names must be byte strings, got: {name!r}")
-
- errors: Dict[Type[Exception], bytes] = {}
- fatalErrors: Dict[Type[Exception], bytes] = {}
- accumulateClassDict(newtype, "errors", errors)
- accumulateClassDict(newtype, "fatalErrors", fatalErrors)
-
- if not isinstance(newtype.errors, dict):
- newtype.errors = dict(newtype.errors)
- if not isinstance(newtype.fatalErrors, dict):
- newtype.fatalErrors = dict(newtype.fatalErrors)
-
- for v, k in errors.items():
- reverseErrors[k] = v
- er[v] = k
- for v, k in fatalErrors.items():
- reverseErrors[k] = v
- er[v] = k
-
- for _, name in newtype.errors.items():
- if not isinstance(name, bytes):
- raise TypeError(f"Error names must be byte strings, got: {name!r}")
- for _, name in newtype.fatalErrors.items():
- if not isinstance(name, bytes):
- raise TypeError(
- f"Fatal error names must be byte strings, got: {name!r}"
- )
-
- return newtype
-
-
- class Command(metaclass=_CommandMeta):
- """
- Subclass me to specify an AMP Command.
-
- @cvar arguments: A list of 2-tuples of (name, Argument-subclass-instance),
- specifying the names and values of the parameters which are required for
- this command.
-
- @cvar response: A list like L{arguments}, but instead used for the return
- value.
-
- @cvar errors: A mapping of subclasses of L{Exception} to wire-protocol tags
- for errors represented as L{str}s. Responders which raise keys from
- this dictionary will have the error translated to the corresponding tag
- on the wire.
- Invokers which receive Deferreds from invoking this command with
- L{BoxDispatcher.callRemote} will potentially receive Failures with keys
- from this mapping as their value.
- This mapping is inherited; if you declare a command which handles
- C{FooError} as 'FOO_ERROR', then subclass it and specify C{BarError} as
- 'BAR_ERROR', responders to the subclass may raise either C{FooError} or
- C{BarError}, and invokers must be able to deal with either of those
- exceptions.
-
- @cvar fatalErrors: like 'errors', but errors in this list will always
- terminate the connection, despite being of a recognizable error type.
-
- @cvar commandType: The type of Box used to issue commands; useful only for
- protocol-modifying behavior like startTLS or protocol switching. Defaults
- to a plain vanilla L{Box}.
-
- @cvar responseType: The type of Box used to respond to this command; only
- useful for protocol-modifying behavior like startTLS or protocol switching.
- Defaults to a plain vanilla L{Box}.
-
- @ivar requiresAnswer: a boolean; defaults to True. Set it to False on your
- subclass if you want callRemote to return None. Note: this is a hint only
- to the client side of the protocol. The return-type of a command responder
- method must always be a dictionary adhering to the contract specified by
- L{response}, because clients are always free to request a response if they
- want one.
- """
-
- arguments: List[Tuple[bytes, Argument]] = []
- response: List[Tuple[bytes, Argument]] = []
- extra: List[Any] = []
- errors: Dict[Type[Exception], bytes] = {}
- fatalErrors: Dict[Type[Exception], bytes] = {}
-
- commandType: "Union[Type[Command], Type[Box]]" = Box
- responseType: Type[AmpBox] = Box
-
- requiresAnswer = True
-
- def __init__(self, **kw):
- """
- Create an instance of this command with specified values for its
- parameters.
-
- In Python 3, keyword arguments MUST be Unicode/native strings whereas
- in Python 2 they could be either byte strings or Unicode strings.
-
- A L{Command}'s arguments are defined in its schema using C{bytes}
- names. The values for those arguments are plucked from the keyword
- arguments using the name returned from L{_wireNameToPythonIdentifier}.
- In other words, keyword arguments should be named using the
- Python-side equivalent of the on-wire (C{bytes}) name.
-
- @param kw: a dict containing an appropriate value for each name
- specified in the L{arguments} attribute of my class.
-
- @raise InvalidSignature: if you forgot any required arguments.
- """
- self.structured = kw
- forgotten = []
- for name, arg in self.arguments:
- pythonName = _wireNameToPythonIdentifier(name)
- if pythonName not in self.structured and not arg.optional:
- forgotten.append(pythonName)
- if forgotten:
- raise InvalidSignature(
- "forgot {} for {}".format(", ".join(forgotten), self.commandName)
- )
- forgotten = []
-
- @classmethod
- def makeResponse(cls, objects, proto):
- """
- Serialize a mapping of arguments using this L{Command}'s
- response schema.
-
- @param objects: a dict with keys matching the names specified in
- self.response, having values of the types that the Argument objects in
- self.response can format.
-
- @param proto: an L{AMP}.
-
- @return: an L{AmpBox}.
- """
- try:
- responseType = cls.responseType()
- except BaseException:
- return fail()
- return _objectsToStrings(objects, cls.response, responseType, proto)
-
- @classmethod
- def makeArguments(cls, objects, proto):
- """
- Serialize a mapping of arguments using this L{Command}'s
- argument schema.
-
- @param objects: a dict with keys similar to the names specified in
- self.arguments, having values of the types that the Argument objects in
- self.arguments can parse.
-
- @param proto: an L{AMP}.
-
- @return: An instance of this L{Command}'s C{commandType}.
- """
- allowedNames = set()
- for (argName, ignored) in cls.arguments:
- allowedNames.add(_wireNameToPythonIdentifier(argName))
-
- for intendedArg in objects:
- if intendedArg not in allowedNames:
- raise InvalidSignature(f"{intendedArg} is not a valid argument")
- return _objectsToStrings(objects, cls.arguments, cls.commandType(), proto)
-
- @classmethod
- def parseResponse(cls, box, protocol):
- """
- Parse a mapping of serialized arguments using this
- L{Command}'s response schema.
-
- @param box: A mapping of response-argument names to the
- serialized forms of those arguments.
- @param protocol: The L{AMP} protocol.
-
- @return: A mapping of response-argument names to the parsed
- forms.
- """
- return _stringsToObjects(box, cls.response, protocol)
-
- @classmethod
- def parseArguments(cls, box, protocol):
- """
- Parse a mapping of serialized arguments using this
- L{Command}'s argument schema.
-
- @param box: A mapping of argument names to the seralized forms
- of those arguments.
- @param protocol: The L{AMP} protocol.
-
- @return: A mapping of argument names to the parsed forms.
- """
- return _stringsToObjects(box, cls.arguments, protocol)
-
- @classmethod
- def responder(cls, methodfunc):
- """
- Declare a method to be a responder for a particular command.
-
- This is a decorator.
-
- Use like so::
-
- class MyCommand(Command):
- arguments = [('a', ...), ('b', ...)]
-
- class MyProto(AMP):
- def myFunMethod(self, a, b):
- ...
- MyCommand.responder(myFunMethod)
-
- Notes: Although decorator syntax is not used within Twisted, this
- function returns its argument and is therefore safe to use with
- decorator syntax.
-
- This is not thread safe. Don't declare AMP subclasses in other
- threads. Don't declare responders outside the scope of AMP subclasses;
- the behavior is undefined.
-
- @param methodfunc: A function which will later become a method, which
- has a keyword signature compatible with this command's L{arguments} list
- and returns a dictionary with a set of keys compatible with this
- command's L{response} list.
-
- @return: the methodfunc parameter.
- """
- CommandLocator._currentClassCommands.append((cls, methodfunc))
- return methodfunc
-
- # Our only instance method
- def _doCommand(self, proto):
- """
- Encode and send this Command to the given protocol.
-
- @param proto: an AMP, representing the connection to send to.
-
- @return: a Deferred which will fire or error appropriately when the
- other side responds to the command (or error if the connection is lost
- before it is responded to).
- """
-
- def _massageError(error):
- error.trap(RemoteAmpError)
- rje = error.value
- errorType = self.reverseErrors.get(rje.errorCode, UnknownRemoteError)
- return Failure(errorType(rje.description))
-
- d = proto._sendBoxCommand(
- self.commandName,
- self.makeArguments(self.structured, proto),
- self.requiresAnswer,
- )
-
- if self.requiresAnswer:
- d.addCallback(self.parseResponse, proto)
- d.addErrback(_massageError)
-
- return d
-
-
- class _NoCertificate:
- """
- This is for peers which don't want to use a local certificate. Used by
- AMP because AMP's internal language is all about certificates and this
- duck-types in the appropriate place; this API isn't really stable though,
- so it's not exposed anywhere public.
-
- For clients, it will use ephemeral DH keys, or whatever the default is for
- certificate-less clients in OpenSSL. For servers, it will generate a
- temporary self-signed certificate with garbage values in the DN and use
- that.
- """
-
- def __init__(self, client):
- """
- Create a _NoCertificate which either is or isn't for the client side of
- the connection.
-
- @param client: True if we are a client and should truly have no
- certificate and be anonymous, False if we are a server and actually
- have to generate a temporary certificate.
-
- @type client: bool
- """
- self.client = client
-
- def options(self, *authorities):
- """
- Behaves like L{twisted.internet.ssl.PrivateCertificate.options}().
- """
- if not self.client:
- # do some crud with sslverify to generate a temporary self-signed
- # certificate. This is SLOOOWWWWW so it is only in the absolute
- # worst, most naive case.
-
- # We have to do this because OpenSSL will not let both the server
- # and client be anonymous.
- sharedDN = DN(CN="TEMPORARY CERTIFICATE")
- key = KeyPair.generate()
- cr = key.certificateRequest(sharedDN)
- sscrd = key.signCertificateRequest(sharedDN, cr, lambda dn: True, 1)
- cert = key.newCertificate(sscrd)
- return cert.options(*authorities)
- options = dict()
- if authorities:
- options.update(
- dict(
- verify=True,
- requireCertificate=True,
- caCerts=[auth.original for auth in authorities],
- )
- )
- occo = CertificateOptions(**options)
- return occo
-
-
- class _TLSBox(AmpBox):
- """
- I am an AmpBox that, upon being sent, initiates a TLS connection.
- """
-
- __slots__: List[str] = []
-
- def __init__(self):
- if ssl is None:
- raise RemoteAmpError(b"TLS_ERROR", "TLS not available")
- AmpBox.__init__(self)
-
- @property
- def certificate(self):
- return self.get(b"tls_localCertificate", _NoCertificate(False))
-
- @property
- def verify(self):
- return self.get(b"tls_verifyAuthorities", None)
-
- def _sendTo(self, proto):
- """
- Send my encoded value to the protocol, then initiate TLS.
- """
- ab = AmpBox(self)
- for k in [b"tls_localCertificate", b"tls_verifyAuthorities"]:
- ab.pop(k, None)
- ab._sendTo(proto)
- proto._startTLS(self.certificate, self.verify)
-
-
- class _LocalArgument(String):
- """
- Local arguments are never actually relayed across the wire. This is just a
- shim so that StartTLS can pretend to have some arguments: if arguments
- acquire documentation properties, replace this with something nicer later.
- """
-
- def fromBox(self, name, strings, objects, proto):
- pass
-
-
- class StartTLS(Command):
- """
- Use, or subclass, me to implement a command that starts TLS.
-
- Callers of StartTLS may pass several special arguments, which affect the
- TLS negotiation:
-
- - tls_localCertificate: This is a
- twisted.internet.ssl.PrivateCertificate which will be used to secure
- the side of the connection it is returned on.
-
- - tls_verifyAuthorities: This is a list of
- twisted.internet.ssl.Certificate objects that will be used as the
- certificate authorities to verify our peer's certificate.
-
- Each of those special parameters may also be present as a key in the
- response dictionary.
- """
-
- arguments = [
- (b"tls_localCertificate", _LocalArgument(optional=True)),
- (b"tls_verifyAuthorities", _LocalArgument(optional=True)),
- ]
-
- response = [
- (b"tls_localCertificate", _LocalArgument(optional=True)),
- (b"tls_verifyAuthorities", _LocalArgument(optional=True)),
- ]
-
- responseType = _TLSBox
-
- def __init__(self, *, tls_localCertificate=None, tls_verifyAuthorities=None, **kw):
- """
- Create a StartTLS command. (This is private. Use AMP.callRemote.)
-
- @param tls_localCertificate: the PrivateCertificate object to use to
- secure the connection. If it's L{None}, or unspecified, an ephemeral DH
- key is used instead.
-
- @param tls_verifyAuthorities: a list of Certificate objects which
- represent root certificates to verify our peer with.
- """
- if ssl is None:
- raise RuntimeError("TLS not available.")
- self.certificate = (
- _NoCertificate(True)
- if tls_localCertificate is None
- else tls_localCertificate
- )
- self.authorities = tls_verifyAuthorities
- Command.__init__(self, **kw)
-
- def _doCommand(self, proto):
- """
- When a StartTLS command is sent, prepare to start TLS, but don't actually
- do it; wait for the acknowledgement, then initiate the TLS handshake.
- """
- d = Command._doCommand(self, proto)
- proto._prepareTLS(self.certificate, self.authorities)
- # XXX before we get back to user code we are going to start TLS...
-
- def actuallystart(response):
- proto._startTLS(self.certificate, self.authorities)
- return response
-
- d.addCallback(actuallystart)
- return d
-
-
- class ProtocolSwitchCommand(Command):
- """
- Use this command to switch from something Amp-derived to a different
- protocol mid-connection. This can be useful to use amp as the
- connection-startup negotiation phase. Since TLS is a different layer
- entirely, you can use Amp to negotiate the security parameters of your
- connection, then switch to a different protocol, and the connection will
- remain secured.
- """
-
- def __init__(self, _protoToSwitchToFactory, **kw):
- """
- Create a ProtocolSwitchCommand.
-
- @param _protoToSwitchToFactory: a ProtocolFactory which will generate
- the Protocol to switch to.
-
- @param kw: Keyword arguments, encoded and handled normally as
- L{Command} would.
- """
-
- self.protoToSwitchToFactory = _protoToSwitchToFactory
- super().__init__(**kw)
-
- @classmethod
- def makeResponse(cls, innerProto, proto):
- return _SwitchBox(innerProto)
-
- def _doCommand(self, proto):
- """
- When we emit a ProtocolSwitchCommand, lock the protocol, but don't actually
- switch to the new protocol unless an acknowledgement is received. If
- an error is received, switch back.
- """
- d = super()._doCommand(proto)
- proto._lockForSwitch()
-
- def switchNow(ign):
- innerProto = self.protoToSwitchToFactory.buildProtocol(
- proto.transport.getPeer()
- )
- proto._switchTo(innerProto, self.protoToSwitchToFactory)
- return ign
-
- def handle(ign):
- proto._unlockFromSwitch()
- self.protoToSwitchToFactory.clientConnectionFailed(
- None, Failure(CONNECTION_LOST)
- )
- return ign
-
- return d.addCallbacks(switchNow, handle)
-
-
- @implementer(IFileDescriptorReceiver)
- class _DescriptorExchanger:
- """
- L{_DescriptorExchanger} is a mixin for L{BinaryBoxProtocol} which adds
- support for receiving file descriptors, a feature offered by
- L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
-
- @ivar _descriptors: Temporary storage for all file descriptors received.
- Values in this dictionary are the file descriptors (as integers). Keys
- in this dictionary are ordinals giving the order in which each
- descriptor was received. The ordering information is used to allow
- L{Descriptor} to determine which is the correct descriptor for any
- particular usage of that argument type.
- @type _descriptors: C{dict}
-
- @ivar _sendingDescriptorCounter: A no-argument callable which returns the
- ordinals, starting from 0. This is used to construct values for
- C{_sendFileDescriptor}.
-
- @ivar _receivingDescriptorCounter: A no-argument callable which returns the
- ordinals, starting from 0. This is used to construct values for
- C{fileDescriptorReceived}.
- """
-
- def __init__(self):
- self._descriptors = {}
- self._getDescriptor = self._descriptors.pop
- self._sendingDescriptorCounter = partial(next, count())
- self._receivingDescriptorCounter = partial(next, count())
-
- def _sendFileDescriptor(self, descriptor):
- """
- Assign and return the next ordinal to the given descriptor after sending
- the descriptor over this protocol's transport.
- """
- self.transport.sendFileDescriptor(descriptor)
- return self._sendingDescriptorCounter()
-
- def fileDescriptorReceived(self, descriptor):
- """
- Collect received file descriptors to be claimed later by L{Descriptor}.
-
- @param descriptor: The received file descriptor.
- @type descriptor: C{int}
- """
- self._descriptors[self._receivingDescriptorCounter()] = descriptor
-
-
- @implementer(IBoxSender)
- class BinaryBoxProtocol(
- StatefulStringProtocol, Int16StringReceiver, _DescriptorExchanger
- ):
- """
- A protocol for receiving L{AmpBox}es - key/value pairs - via length-prefixed
- strings. A box is composed of:
-
- - any number of key-value pairs, described by:
- - a 2-byte network-endian packed key length (of which the first
- byte must be null, and the second must be non-null: i.e. the
- value of the length must be 1-255)
- - a key, comprised of that many bytes
- - a 2-byte network-endian unsigned value length (up to the maximum
- of 65535)
- - a value, comprised of that many bytes
- - 2 null bytes
-
- In other words, an even number of strings prefixed with packed unsigned
- 16-bit integers, and then a 0-length string to indicate the end of the box.
-
- This protocol also implements 2 extra private bits of functionality related
- to the byte boundaries between messages; it can start TLS between two given
- boxes or switch to an entirely different protocol. However, due to some
- tricky elements of the implementation, the public interface to this
- functionality is L{ProtocolSwitchCommand} and L{StartTLS}.
-
- @ivar _keyLengthLimitExceeded: A flag which is only true when the
- connection is being closed because a key length prefix which was longer
- than allowed by the protocol was received.
-
- @ivar boxReceiver: an L{IBoxReceiver} provider, whose
- L{IBoxReceiver.ampBoxReceived} method will be invoked for each
- L{AmpBox} that is received.
- """
-
- _justStartedTLS = False
- _startingTLSBuffer = None
- _locked = False
- _currentKey = None
- _currentBox = None
-
- _keyLengthLimitExceeded = False
-
- hostCertificate = None
- noPeerCertificate = False # for tests
- innerProtocol: Optional[Protocol] = None
- innerProtocolClientFactory = None
-
- def __init__(self, boxReceiver):
- _DescriptorExchanger.__init__(self)
- self.boxReceiver = boxReceiver
-
- def _switchTo(self, newProto, clientFactory=None):
- """
- Switch this BinaryBoxProtocol's transport to a new protocol. You need
- to do this 'simultaneously' on both ends of a connection; the easiest
- way to do this is to use a subclass of ProtocolSwitchCommand.
-
- @param newProto: the new protocol instance to switch to.
-
- @param clientFactory: the ClientFactory to send the
- L{twisted.internet.protocol.ClientFactory.clientConnectionLost}
- notification to.
- """
- # All the data that Int16Receiver has not yet dealt with belongs to our
- # new protocol: luckily it's keeping that in a handy (although
- # ostensibly internal) variable for us:
- newProtoData = self.recvd
- # We're quite possibly in the middle of a 'dataReceived' loop in
- # Int16StringReceiver: let's make sure that the next iteration, the
- # loop will break and not attempt to look at something that isn't a
- # length prefix.
- self.recvd = ""
- # Finally, do the actual work of setting up the protocol and delivering
- # its first chunk of data, if one is available.
- self.innerProtocol = newProto
- self.innerProtocolClientFactory = clientFactory
- newProto.makeConnection(self.transport)
- if newProtoData:
- newProto.dataReceived(newProtoData)
-
- def sendBox(self, box):
- """
- Send a amp.Box to my peer.
-
- Note: transport.write is never called outside of this method.
-
- @param box: an AmpBox.
-
- @raise ProtocolSwitched: if the protocol has previously been switched.
-
- @raise ConnectionLost: if the connection has previously been lost.
- """
- if self._locked:
- raise ProtocolSwitched(
- "This connection has switched: no AMP traffic allowed."
- )
- if self.transport is None:
- raise ConnectionLost()
- if self._startingTLSBuffer is not None:
- self._startingTLSBuffer.append(box)
- else:
- self.transport.write(box.serialize())
-
- def makeConnection(self, transport):
- """
- Notify L{boxReceiver} that it is about to receive boxes from this
- protocol by invoking L{IBoxReceiver.startReceivingBoxes}.
- """
- self.transport = transport
- self.boxReceiver.startReceivingBoxes(self)
- self.connectionMade()
-
- def dataReceived(self, data):
- """
- Either parse incoming data as L{AmpBox}es or relay it to our nested
- protocol.
- """
- if self._justStartedTLS:
- self._justStartedTLS = False
- # If we already have an inner protocol, then we don't deliver data to
- # the protocol parser any more; we just hand it off.
- if self.innerProtocol is not None:
- self.innerProtocol.dataReceived(data)
- return
- return Int16StringReceiver.dataReceived(self, data)
-
- def connectionLost(self, reason):
- """
- The connection was lost; notify any nested protocol.
- """
- if self.innerProtocol is not None:
- self.innerProtocol.connectionLost(reason)
- if self.innerProtocolClientFactory is not None:
- self.innerProtocolClientFactory.clientConnectionLost(None, reason)
- if self._keyLengthLimitExceeded:
- failReason = Failure(TooLong(True, False, None, None))
- elif reason.check(ConnectionClosed) and self._justStartedTLS:
- # We just started TLS and haven't received any data. This means
- # the other connection didn't like our cert (although they may not
- # have told us why - later Twisted should make 'reason' into a TLS
- # error.)
- failReason = PeerVerifyError(
- "Peer rejected our certificate for an unknown reason."
- )
- else:
- failReason = reason
- self.boxReceiver.stopReceivingBoxes(failReason)
-
- # The longest key allowed
- _MAX_KEY_LENGTH = 255
-
- # The longest value allowed (this is somewhat redundant, as longer values
- # cannot be encoded - ah well).
- _MAX_VALUE_LENGTH = 65535
-
- # The first thing received is a key.
- MAX_LENGTH = _MAX_KEY_LENGTH
-
- def proto_init(self, string):
- """
- String received in the 'init' state.
- """
- self._currentBox = AmpBox()
- return self.proto_key(string)
-
- def proto_key(self, string):
- """
- String received in the 'key' state. If the key is empty, a complete
- box has been received.
- """
- if string:
- self._currentKey = string
- self.MAX_LENGTH = self._MAX_VALUE_LENGTH
- return "value"
- else:
- self.boxReceiver.ampBoxReceived(self._currentBox)
- self._currentBox = None
- return "init"
-
- def proto_value(self, string):
- """
- String received in the 'value' state.
- """
- self._currentBox[self._currentKey] = string
- self._currentKey = None
- self.MAX_LENGTH = self._MAX_KEY_LENGTH
- return "key"
-
- def lengthLimitExceeded(self, length):
- """
- The key length limit was exceeded. Disconnect the transport and make
- sure a meaningful exception is reported.
- """
- self._keyLengthLimitExceeded = True
- self.transport.loseConnection()
-
- def _lockForSwitch(self):
- """
- Lock this binary protocol so that no further boxes may be sent. This
- is used when sending a request to switch underlying protocols. You
- probably want to subclass ProtocolSwitchCommand rather than calling
- this directly.
- """
- self._locked = True
-
- def _unlockFromSwitch(self):
- """
- Unlock this locked binary protocol so that further boxes may be sent
- again. This is used after an attempt to switch protocols has failed
- for some reason.
- """
- if self.innerProtocol is not None:
- raise ProtocolSwitched("Protocol already switched. Cannot unlock.")
- self._locked = False
-
- def _prepareTLS(self, certificate, verifyAuthorities):
- """
- Used by StartTLSCommand to put us into the state where we don't
- actually send things that get sent, instead we buffer them. see
- L{_sendBoxCommand}.
- """
- self._startingTLSBuffer = []
- if self.hostCertificate is not None:
- raise OnlyOneTLS(
- "Previously authenticated connection between %s and %s "
- "is trying to re-establish as %s"
- % (
- self.hostCertificate,
- self.peerCertificate,
- (certificate, verifyAuthorities),
- )
- )
-
- def _startTLS(self, certificate, verifyAuthorities):
- """
- Used by TLSBox to initiate the SSL handshake.
-
- @param certificate: a L{twisted.internet.ssl.PrivateCertificate} for
- use locally.
-
- @param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances
- representing certificate authorities which will verify our peer.
- """
- self.hostCertificate = certificate
- self._justStartedTLS = True
- if verifyAuthorities is None:
- verifyAuthorities = ()
- self.transport.startTLS(certificate.options(*verifyAuthorities))
- stlsb = self._startingTLSBuffer
- if stlsb is not None:
- self._startingTLSBuffer = None
- for box in stlsb:
- self.sendBox(box)
-
- @property
- def peerCertificate(self):
- if self.noPeerCertificate:
- return None
- return Certificate.peerFromTransport(self.transport)
-
- def unhandledError(self, failure):
- """
- The buck stops here. This error was completely unhandled, time to
- terminate the connection.
- """
- log.err(
- failure,
- "Amp server or network failure unhandled by client application. "
- "Dropping connection! To avoid, add errbacks to ALL remote "
- "commands!",
- )
- if self.transport is not None:
- self.transport.loseConnection()
-
- def _defaultStartTLSResponder(self):
- """
- The default TLS responder doesn't specify any certificate or anything.
-
- From a security perspective, it's little better than a plain-text
- connection - but it is still a *bit* better, so it's included for
- convenience.
-
- You probably want to override this by providing your own StartTLS.responder.
- """
- return {}
-
- StartTLS.responder(_defaultStartTLSResponder)
-
-
- class AMP(BinaryBoxProtocol, BoxDispatcher, CommandLocator, SimpleStringLocator):
- """
- This protocol is an AMP connection. See the module docstring for protocol
- details.
- """
-
- _ampInitialized = False
-
- def __init__(self, boxReceiver=None, locator=None):
- # For backwards compatibility. When AMP did not separate parsing logic
- # (L{BinaryBoxProtocol}), request-response logic (L{BoxDispatcher}) and
- # command routing (L{CommandLocator}), it did not have a constructor.
- # Now it does, so old subclasses might have defined their own that did
- # not upcall. If this flag isn't set, we'll call the constructor in
- # makeConnection before anything actually happens.
- self._ampInitialized = True
- if boxReceiver is None:
- boxReceiver = self
- if locator is None:
- locator = self
- BoxDispatcher.__init__(self, locator)
- BinaryBoxProtocol.__init__(self, boxReceiver)
-
- def locateResponder(self, name):
- """
- Unify the implementations of L{CommandLocator} and
- L{SimpleStringLocator} to perform both kinds of dispatch, preferring
- L{CommandLocator}.
-
- @type name: C{bytes}
- """
- firstResponder = CommandLocator.locateResponder(self, name)
- if firstResponder is not None:
- return firstResponder
- secondResponder = SimpleStringLocator.locateResponder(self, name)
- return secondResponder
-
- def __repr__(self) -> str:
- """
- A verbose string representation which gives us information about this
- AMP connection.
- """
- if self.innerProtocol is not None:
- innerRepr = f" inner {self.innerProtocol!r}"
- else:
- innerRepr = ""
- return f"<{self.__class__.__name__}{innerRepr} at 0x{id(self):x}>"
-
- def makeConnection(self, transport):
- """
- Emit a helpful log message when the connection is made.
- """
- if not self._ampInitialized:
- # See comment in the constructor re: backward compatibility. I
- # should probably emit a deprecation warning here.
- AMP.__init__(self)
- # Save these so we can emit a similar log message in L{connectionLost}.
- self._transportPeer = transport.getPeer()
- self._transportHost = transport.getHost()
- log.msg(
- "%s connection established (HOST:%s PEER:%s)"
- % (self.__class__.__name__, self._transportHost, self._transportPeer)
- )
- BinaryBoxProtocol.makeConnection(self, transport)
-
- def connectionLost(self, reason):
- """
- Emit a helpful log message when the connection is lost.
- """
- log.msg(
- "%s connection lost (HOST:%s PEER:%s)"
- % (self.__class__.__name__, self._transportHost, self._transportPeer)
- )
- BinaryBoxProtocol.connectionLost(self, reason)
- self.transport = None
-
-
- class _ParserHelper:
- """
- A box receiver which records all boxes received.
- """
-
- def __init__(self):
- self.boxes = []
-
- def getPeer(self):
- return "string"
-
- def getHost(self):
- return "string"
-
- disconnecting = False
-
- def startReceivingBoxes(self, sender):
- """
- No initialization is required.
- """
-
- def ampBoxReceived(self, box):
- self.boxes.append(box)
-
- # Synchronous helpers
- @classmethod
- def parse(cls, fileObj):
- """
- Parse some amp data stored in a file.
-
- @param fileObj: a file-like object.
-
- @return: a list of AmpBoxes encoded in the given file.
- """
- parserHelper = cls()
- bbp = BinaryBoxProtocol(boxReceiver=parserHelper)
- bbp.makeConnection(parserHelper)
- bbp.dataReceived(fileObj.read())
- return parserHelper.boxes
-
- @classmethod
- def parseString(cls, data):
- """
- Parse some amp data stored in a string.
-
- @param data: a str holding some amp-encoded data.
-
- @return: a list of AmpBoxes encoded in the given string.
- """
- return cls.parse(BytesIO(data))
-
-
- parse = _ParserHelper.parse
- parseString = _ParserHelper.parseString
-
-
- def _stringsToObjects(strings, arglist, proto):
- """
- Convert an AmpBox to a dictionary of python objects, converting through a
- given arglist.
-
- @param strings: an AmpBox (or dict of strings)
-
- @param arglist: a list of 2-tuples of strings and Argument objects, as
- described in L{Command.arguments}.
-
- @param proto: an L{AMP} instance.
-
- @return: the converted dictionary mapping names to argument objects.
- """
- objects = {}
- myStrings = strings.copy()
- for argname, argparser in arglist:
- argparser.fromBox(argname, myStrings, objects, proto)
- return objects
-
-
- def _objectsToStrings(objects, arglist, strings, proto):
- """
- Convert a dictionary of python objects to an AmpBox, converting through a
- given arglist.
-
- @param objects: a dict mapping names to python objects
-
- @param arglist: a list of 2-tuples of strings and Argument objects, as
- described in L{Command.arguments}.
-
- @param strings: [OUT PARAMETER] An object providing the L{dict}
- interface which will be populated with serialized data.
-
- @param proto: an L{AMP} instance.
-
- @return: The converted dictionary mapping names to encoded argument
- strings (identical to C{strings}).
- """
- myObjects = objects.copy()
- for argname, argparser in arglist:
- argparser.toBox(argname, strings, myObjects, proto)
- return strings
-
-
- class Decimal(Argument):
- """
- Encodes C{decimal.Decimal} instances.
-
- There are several ways in which a decimal value might be encoded.
-
- Special values are encoded as special strings::
-
- - Positive infinity is encoded as C{"Infinity"}
- - Negative infinity is encoded as C{"-Infinity"}
- - Quiet not-a-number is encoded as either C{"NaN"} or C{"-NaN"}
- - Signalling not-a-number is encoded as either C{"sNaN"} or C{"-sNaN"}
-
- Normal values are encoded using the base ten string representation, using
- engineering notation to indicate magnitude without precision, and "normal"
- digits to indicate precision. For example::
-
- - C{"1"} represents the value I{1} with precision to one place.
- - C{"-1"} represents the value I{-1} with precision to one place.
- - C{"1.0"} represents the value I{1} with precision to two places.
- - C{"10"} represents the value I{10} with precision to two places.
- - C{"1E+2"} represents the value I{10} with precision to one place.
- - C{"1E-1"} represents the value I{0.1} with precision to one place.
- - C{"1.5E+2"} represents the value I{15} with precision to two places.
-
- U{http://speleotrove.com/decimal/} should be considered the authoritative
- specification for the format.
- """
-
- def fromString(self, inString):
- inString = nativeString(inString)
- return decimal.Decimal(inString)
-
- def toString(self, inObject):
- """
- Serialize a C{decimal.Decimal} instance to the specified wire format.
- """
- if isinstance(inObject, decimal.Decimal):
- # Hopefully decimal.Decimal.__str__ actually does what we want.
- return str(inObject).encode("ascii")
- raise ValueError("amp.Decimal can only encode instances of decimal.Decimal")
-
-
- class DateTime(Argument):
- """
- Encodes C{datetime.datetime} instances.
-
- Wire format: '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i'. Fields in
- order are: year, month, day, hour, minute, second, microsecond, timezone
- direction (+ or -), timezone hour, timezone minute. Encoded string is
- always exactly 32 characters long. This format is compatible with ISO 8601,
- but that does not mean all ISO 8601 dates can be accepted.
-
- Also, note that the datetime module's notion of a "timezone" can be
- complex, but the wire format includes only a fixed offset, so the
- conversion is not lossless. A lossless transmission of a C{datetime} instance
- is not feasible since the receiving end would require a Python interpreter.
-
- @ivar _positions: A sequence of slices giving the positions of various
- interesting parts of the wire format.
- """
-
- _positions = [
- slice(0, 4),
- slice(5, 7),
- slice(8, 10), # year, month, day
- slice(11, 13),
- slice(14, 16),
- slice(17, 19), # hour, minute, second
- slice(20, 26), # microsecond
- # intentionally skip timezone direction, as it is not an integer
- slice(27, 29),
- slice(30, 32), # timezone hour, timezone minute
- ]
-
- def fromString(self, s):
- """
- Parse a string containing a date and time in the wire format into a
- C{datetime.datetime} instance.
- """
- s = nativeString(s)
-
- if len(s) != 32:
- raise ValueError(f"invalid date format {s!r}")
-
- values = [int(s[p]) for p in self._positions]
- sign = s[26]
- timezone = _FixedOffsetTZInfo.fromSignHoursMinutes(sign, *values[7:])
- values[7:] = [timezone]
- return datetime.datetime(*values)
-
- def toString(self, i):
- """
- Serialize a C{datetime.datetime} instance to a string in the specified
- wire format.
- """
- offset = i.utcoffset()
- if offset is None:
- raise ValueError(
- "amp.DateTime cannot serialize naive datetime instances. "
- "You may find amp.utc useful."
- )
-
- minutesOffset = (offset.days * 86400 + offset.seconds) // 60
-
- if minutesOffset > 0:
- sign = "+"
- else:
- sign = "-"
-
- # strftime has no way to format the microseconds, or put a ':' in the
- # timezone. Surprise!
-
- # Python 3.4 cannot do % interpolation on byte strings so we pack into
- # an explicitly Unicode string then encode as ASCII.
- packed = "%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i" % (
- i.year,
- i.month,
- i.day,
- i.hour,
- i.minute,
- i.second,
- i.microsecond,
- sign,
- abs(minutesOffset) // 60,
- abs(minutesOffset) % 60,
- )
-
- return packed.encode("ascii")
|