Package pyamf :: Package remoting

[hide private]
[frames] | no frames]

# Source Code for Package pyamf.remoting

      1  # Copyright (c) 2007-2009 The PyAMF Project.
      2  # See LICENSE.txt for details.
      3
      4  """
      5  AMF Remoting support.
      6
      7  A Remoting request from the client consists of a short preamble, headers, and
      8  bodies. The preamble contains basic information about the nature of the
      9  request. Headers can be used to request debugging information, send
     10  authentication info, tag transactions, etc. Bodies contain actual Remoting
     11  requests and responses. A single Remoting envelope can contain several
     12  requests; Remoting supports batching out of the box.
     13
     14  Client headers and bodies need not be responded to in a one-to-one manner.
     15  That is, a body or header may not require a response. Debug information is
     16  requested by a header but sent back as a body object. The response index is
     17  essential for the Adobe Flash Player to understand the response therefore.
     18
     19  @see: U{Remoting Envelope on OSFlash (external)
     20  <http://osflash.org/documentation/amf/envelopes/remoting>}
     21  @see: U{Remoting Headers on OSFlash (external)
     22  <http://osflash.org/amf/envelopes/remoting/headers>}
     23  @see: U{Remoting Debug Headers on OSFlash (external)
     24  <http://osflash.org/documentation/amf/envelopes/remoting/debuginfo>}
     25
     26  @since: 0.1.0
     27  """
     28
     29  import pyamf
     30  from pyamf import util
     31
     32  __all__ = ['Envelope', 'Request', 'Response', 'decode', 'encode']
     33
     34  #: Succesful call.
     35  STATUS_OK = 0
     36  #: Reserved for runtime errors.
     37  STATUS_ERROR = 1
     38  #: Debug information.
     39  STATUS_DEBUG = 2
     40
     41  #: List of available status response codes.
     42  STATUS_CODES = {
     43      STATUS_OK:    '/onResult',
     44      STATUS_ERROR: '/onStatus',
     45      STATUS_DEBUG: '/onDebugEvents'
     46  }
     47
     48  #: AMF mimetype.
     49  CONTENT_TYPE = 'application/x-amf'
     50
     51  ERROR_CALL_FAILED, = range(1)
     52  ERROR_CODES = {
     53      ERROR_CALL_FAILED: 'Server.Call.Failed'
     54  }
     55
     56  APPEND_TO_GATEWAY_URL = 'AppendToGatewayUrl'
     57  REPLACE_GATEWAY_URL = 'ReplaceGatewayUrl'
     58  REQUEST_PERSISTENT_HEADER = 'RequestPersistentHeader'
     59
     60
     61 -class RemotingError(pyamf.BaseError):
     62      """
     63      Generic remoting error class.
     64      """
     65
     66
     67 -class RemotingCallFailed(RemotingError):
     68      """
     69      Raised if C{Server.Call.Failed} received.
     70      """
     71
     72  pyamf.add_error_class(RemotingCallFailed, ERROR_CODES[ERROR_CALL_FAILED])
     73
     74
     75 -class HeaderCollection(dict):
     76      """
     77      Collection of AMF message headers.
     78      """
     79
     80 -    def __init__(self, raw_headers={}):
     81          self.required = []
     82
     83          for (k, ig, v) in raw_headers:
     84              self[k] = v
     85              if ig:
     86                  self.required.append(k)
     87
     88 -    def is_required(self, idx):
     89          """
     90          @raise KeyError: Unknown header found.
     91          """
     92          if not idx in self:
     93              raise KeyError("Unknown header %s" % str(idx))
     94
     95          return idx in self.required
     96
     97 -    def set_required(self, idx, value=True):
     98          """
     99          @raise KeyError: Unknown header found.
    100          """
    101          if not idx in self:
    102              raise KeyError("Unknown header %s" % str(idx))
    103
    104          if not idx in self.required:
    105              self.required.append(idx)
    106
    107 -    def __len__(self):
    108          return len(self.keys())
    109
    110
    111 -class Envelope(object):
    112      """
    113      I wrap an entire request, encapsulating headers and bodies.
    114
    115      There can be more than one request in a single transaction.
    116
    117      @ivar amfVersion: AMF encoding version. See L{pyamf.ENCODING_TYPES}
    118      @type amfVersion: C{int} or C{None}
    119      @ivar clientType: Client type. See L{ClientTypes<pyamf.ClientTypes>}
    120      @type clientType: C{int} or C{None}
    121      @ivar headers: AMF headers, a list of name, value pairs. Global to each
    122          request.
    123      @type headers: L{HeaderCollection}
    124      @ivar bodies: A list of requests/response messages
    125      @type bodies: L{list} containing tuples of the key of the request and
    126          the instance of the L{Message}
    127      """
    128
    129 -    def __init__(self, amfVersion=None, clientType=None):
    130          self.amfVersion = amfVersion
    131          self.clientType = clientType
    132          self.headers = HeaderCollection()
    133          self.bodies = []
    134
    135 -    def __repr__(self):
    136          r = "<Envelope amfVersion=%s clientType=%s>\n" % (
    137              self.amfVersion, self.clientType)
    138
    139          for h in self.headers:
    140              r += " " + repr(h) + "\n"
    141
    142          for request in iter(self):
    143              r += " " + repr(request) + "\n"
    144
    145          r += "</Envelope>"
    146
    147          return r
    148
    149 -    def __setitem__(self, name, value):
    150          if not isinstance(value, Message):
    151              raise TypeError("Message instance expected")
    152
    153          idx = 0
    154          found = False
    155
    156          for body in self.bodies:
    157              if name == body[0]:
    158                  self.bodies[idx] = (name, value)
    159                  found = True
    160
    161              idx = idx + 1
    162
    163          if not found:
    164              self.bodies.append((name, value))
    165
    166          value.envelope = self
    167
    168 -    def __getitem__(self, name):
    169          for body in self.bodies:
    170              if name == body[0]:
    171                  return body[1]
    172
    173          raise KeyError("'%r'" % (name,))
    174
    175 -    def __iter__(self):
    176          for body in self.bodies:
    177              yield body[0], body[1]
    178
    179          raise StopIteration
    180
    181 -    def __len__(self):
    182          return len(self.bodies)
    183
    184 -    def iteritems(self):
    185          for body in self.bodies:
    186              yield body
    187
    188          raise StopIteration
    189
    190 -    def keys(self):
    191          return [body[0] for body in self.bodies]
    192
    193 -    def items(self):
    194          return self.bodies
    195
    196 -    def __contains__(self, name):
    197          for body in self.bodies:
    198              if name == body[0]:
    199                  return True
    200
    201          return False
    202
    203 -    def __eq__(self, other):
    204          if isinstance(other, Envelope):
    205              return (self.amfVersion == other.amfVersion and
    206                  self.clientType == other.clientType and
    207                  self.headers == other.headers and
    208                  self.bodies == other.bodies)
    209
    210          if hasattr(other, 'keys') and hasattr(other, 'items'):
    211              keys, o_keys = self.keys(), other.keys()
    212
    213              if len(o_keys) != len(keys):
    214                  return False
    215
    216              for k in o_keys:
    217                  if k not in keys:
    218                      return False
    219
    220                  keys.remove(k)
    221
    222              for k, v in other.items():
    223                  if self[k] != v:
    224                      return False
    225
    226              return True
    227
    228
    229 -class Message(object):
    230      """
    231      I represent a singular request/response, containing a collection of
    232      headers and one body of data.
    233
    234      I am used to iterate over all requests in the L{Envelope}.
    235
    236      @ivar envelope: The parent envelope of this AMF Message.
    237      @type envelope: L{Envelope}
    238      @ivar body: The body of the message.
    239      @type body: C{mixed}
    240      @ivar headers: The message headers.
    241      @type headers: C{dict}
    242      """
    243
    244 -    def __init__(self, envelope, body):
    245          self.envelope = envelope
    246          self.body = body
    247
    248 -    def _get_headers(self):
    249          return self.envelope.headers
    250
    251      headers = property(_get_headers)
    252
    253
    254 -class Request(Message):
    255      """
    256      An AMF Request payload.
    257
    258      @ivar target: The target of the request
    259      @type target: C{basestring}
    260      """
    261
    262 -    def __init__(self, target, body=[], envelope=None):
    263          Message.__init__(self, envelope, body)
    264
    265          self.target = target
    266
    267 -    def __repr__(self):
    268          return "<%s target=%s>%s</%s>" % (
    269              type(self).__name__, repr(self.target), repr(self.body), type(self).__name__)
    270
    271
    272 -class Response(Message):
    273      """
    274      An AMF Response.
    275
    276      @ivar status: The status of the message. Default is L{STATUS_OK}.
    277      @type status: Member of L{STATUS_CODES}.
    278      """
    279
    280 -    def __init__(self, body, status=STATUS_OK, envelope=None):
    281          Message.__init__(self, envelope, body)
    282
    283          self.status = status
    284
    285 -    def __repr__(self):
    286          return "<%s status=%s>%s</%s>" % (
    287              type(self).__name__, _get_status(self.status), repr(self.body),
    288              type(self).__name__
    289          )
    290
    291
    292 -class BaseFault(object):
    293      """
    294      I represent a C{Fault} message (C{mx.rpc.Fault}).
    295
    296      @ivar level: The level of the fault.
    297      @type level: C{str}
    298      @ivar code: A simple code describing the fault.
    299      @type code: C{str}
    300      @ivar details: Any extra details of the fault.
    301      @type details: C{str}
    302      @ivar description: Text description of the fault.
    303      @type description: C{str}
    304
    305      @see: U{mx.rpc.Fault on Livedocs (external)
    306      <http://livedocs.adobe.com/flex/201/langref/mx/rpc/Fault.html>}
    307      """
    308
    309      level = None
    310
    311 -    class __amf__:
    312          static = ('level', 'code', 'type', 'details', 'description')
    313
    314 -    def __init__(self, *args, **kwargs):
    315          self.code = kwargs.get('code', '')
    316          self.type = kwargs.get('type', '')
    317          self.details = kwargs.get('details', '')
    318          self.description = kwargs.get('description', '')
    319
    320 -    def __repr__(self):
    321          x = '%s level=%s' % (self.__class__.__name__, self.level)
    322
    323          if self.code not in ('', None):
    324              x += ' code=%s' % repr(self.code)
    325          if self.type not in ('', None):
    326              x += ' type=%s' % repr(self.type)
    327          if self.description not in ('', None):
    328              x += ' description=%s' % repr(self.description)
    329
    330          if self.details not in ('', None):
    331              x += '\nTraceback:\n%s' % (repr(self.details),)
    332
    333          return x
    334
    335 -    def raiseException(self):
    336          """
    337          Raises an exception based on the fault object. There is no traceback
    338          available.
    339          """
    340          raise get_exception_from_fault(self), self.description, None
    341
    342
    343 -class ErrorFault(BaseFault):
    344      """
    345      I represent an error level fault.
    346      """
    347
    348      level = 'error'
    349
    350
    351 -def _read_header(stream, decoder, strict=False):
    352      """
    353      Read AMF L{Message} header.
    354
    355      @type   stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
    356      @param  stream: AMF data.
    357      @type   decoder: L{amf0.Decoder<pyamf.amf0.Decoder>}
    358      @param  decoder: AMF decoder instance
    359      @type strict: C{bool}
    360      @param strict: Use strict decoding policy. Default is C{False}.
    361      @raise DecodeError: The data that was read from the stream
    362      does not match the header length.
    363
    364      @rtype: C{tuple}
    365      @return:
    366       - Name of the header.
    367       - A C{bool} determining if understanding this header is
    368       required.
    369       - Value of the header.
    370      """
    371      name_len = stream.read_ushort()
    372      name = stream.read_utf8_string(name_len)
    373
    374      required = bool(stream.read_uchar())
    375
    376      data_len = stream.read_ulong()
    377      pos = stream.tell()
    378
    379      data = decoder.readElement()
    380
    381      if strict and pos + data_len != stream.tell():
    382          raise pyamf.DecodeError(
    383              "Data read from stream does not match header length")
    384
    385      return (name, required, data)
    386
    387
    388 -def _write_header(name, header, required, stream, encoder, strict=False):
    389      """
    390      Write AMF message header.
    391
    392      @type   name: C{str}
    393      @param  name: Name of the header.
    394      @type   header:
    395      @param  header: Raw header data.
    396      @type   required: L{bool}
    397      @param  required: Required header.
    398      @type   stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
    399      @param  stream: AMF data.
    400      @type   encoder: L{amf0.Encoder<pyamf.amf0.Encoder>}
    401      or L{amf3.Encoder<pyamf.amf3.Encoder>}
    402      @param  encoder: AMF encoder instance.
    403      @type strict: C{bool}
    404      @param strict: Use strict encoding policy. Default is C{False}.
    405      """
    406      stream.write_ushort(len(name))
    407      stream.write_utf8_string(name)
    408
    409      stream.write_uchar(required)
    410      write_pos = stream.tell()
    411
    412      stream.write_ulong(0)
    413      old_pos = stream.tell()
    414      encoder.writeElement(header)
    415      new_pos = stream.tell()
    416
    417      if strict:
    418          stream.seek(write_pos)
    419          stream.write_ulong(new_pos - old_pos)
    420          stream.seek(new_pos)
    421
    422
    423 -def _read_body(stream, decoder, strict=False, logger=None):
    424      """
    425      Read AMF message body.
    426
    427      @param stream: AMF data.
    428      @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
    429      @param decoder: AMF decoder instance.
    430      @type decoder: L{amf0.Decoder<pyamf.amf0.Decoder>}
    431      @param strict: Use strict decoding policy. Default is C{False}.
    432      @type strict: C{bool}
    433      @raise DecodeError: Data read from stream does not match body length.
    434      @param logger: Used to log interesting events whilst reading a remoting
    435          body.
    436      @type logger: A L{logging.Logger} instance or C{None}.
    437
    438      @rtype: C{tuple}
    439      @return: A C{tuple} containing:
    440          - ID of the request
    441          - L{Request} or L{Response}
    442      """
    443      def _read_args():
    444          """
    445          @raise pyamf.DecodeError: Array type required for request body.
    446          """
    447          if stream.read(1) != '\x0a':
    448              raise pyamf.DecodeError("Array type required for request body")
    449
    450          x = stream.read_ulong()
    451
    452          return [decoder.readElement() for i in xrange(x)]
    453
    454      target = stream.read_utf8_string(stream.read_ushort())
    455      response = stream.read_utf8_string(stream.read_ushort())
    456
    457      status = STATUS_OK
    458      is_request = True
    459
    460      for code, s in STATUS_CODES.iteritems():
    461          if not target.endswith(s):
    462              continue
    463
    464          is_request = False
    465          status = code
    466          target = target[:0 - len(s)]
    467
    468      if logger:
    469          logger.debug('Remoting target: %r' % (target,))
    470
    471      data_len = stream.read_ulong()
    472      pos = stream.tell()
    473
    474      if is_request:
    475          data = _read_args()
    476      else:
    477          data = decoder.readElement()
    478
    479      if strict and pos + data_len != stream.tell():
    480          raise pyamf.DecodeError("Data read from stream does not match body "
    481              "length (%d != %d)" % (pos + data_len, stream.tell(),))
    482
    483      if is_request:
    484          return response, Request(target, body=data)
    485
    486      if status == STATUS_ERROR and isinstance(data, pyamf.ASObject):
    487          data = get_fault(data)
    488
    489      return target, Response(data, status)
    490
    491
    492 -def _write_body(name, message, stream, encoder, strict=False):
    493      """
    494      Write AMF message body.
    495
    496      @param name: The name of the request.
    497      @type name: C{basestring}
    498      @param message: The AMF payload.
    499      @type message: L{Request} or L{Response}
    500      @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
    501      @type encoder: L{amf0.Encoder<pyamf.amf0.Encoder>}
    502      @param encoder: Encoder to use.
    503      @type strict: C{bool}
    504      @param strict: Use strict encoding policy. Default is C{False}.
    505
    506      @raise TypeError: Unknown message type for C{message}.
    507      """
    508      def _encode_body(message):
    509          if isinstance(message, Response):
    510              encoder.writeElement(message.body)
    511
    512              return
    513
    514          stream.write('\x0a')
    515          stream.write_ulong(len(message.body))
    516          for x in message.body:
    517              encoder.writeElement(x)
    518
    519      if not isinstance(message, (Request, Response)):
    520          raise TypeError("Unknown message type")
    521
    522      target = None
    523
    524      if isinstance(message, Request):
    525          target = unicode(message.target)
    526      else:
    527          target = u"%s%s" % (name, _get_status(message.status))
    528
    529      target = target.encode('utf8')
    530
    531      stream.write_ushort(len(target))
    532      stream.write_utf8_string(target)
    533
    534      response = 'null'
    535
    536      if isinstance(message, Request):
    537          response = name
    538
    539      stream.write_ushort(len(response))
    540      stream.write_utf8_string(response)
    541
    542      if not strict:
    543          stream.write_ulong(0)
    544          _encode_body(message)
    545
    546          return
    547
    548      write_pos = stream.tell()
    549      stream.write_ulong(0)
    550      old_pos = stream.tell()
    551
    552      _encode_body(message)
    553      new_pos = stream.tell()
    554
    555      stream.seek(write_pos)
    556      stream.write_ulong(new_pos - old_pos)
    557      stream.seek(new_pos)
    558
    559
    560 -def _get_status(status):
    561      """
    562      Get status code.
    563
    564      @type status: C{str}
    565      @raise ValueError: The status code is unknown.
    566      @return: Status code.
    567      @see: L{STATUS_CODES}
    568      """
    569      if status not in STATUS_CODES.keys():
    570          # TODO print that status code..
    571          raise ValueError("Unknown status code")
    572
    573      return STATUS_CODES[status]
    574
    575
    576 -def get_fault_class(level, **kwargs):
    577      if level == 'error':
    578          return ErrorFault
    579
    580      return BaseFault
    581
    582
    583 -def get_fault(data):
    584      try:
    585          level = data['level']
    586          del data['level']
    587      except KeyError:
    588          level = 'error'
    589
    590      e = {}
    591
    592      for x, y in data.iteritems():
    593          if isinstance(x, unicode):
    594              e[str(x)] = y
    595          else:
    596              e[x] = y
    597
    598      return get_fault_class(level, **e)(**e)
    599
    600
    601 -def decode(stream, context=None, strict=False, logger=None, timezone_offset=None):
    602      """
    603      Decodes the incoming stream as a remoting message.
    604
    605      @param stream: AMF data.
    606      @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
    607      @param context: Context.
    608      @type context: L{amf0.Context<pyamf.amf0.Context>} or
    609      L{amf3.Context<pyamf.amf3.Context>}
    610      @param strict: Enforce strict decoding. Default is C{False}.
    611      @type strict: C{bool}
    612      @param logger: Used to log interesting events whilst decoding a remoting
    613          message.
    614      @type logger: A L{logging.Logger} instance or C{None}.
    615      @param timezone_offset: The difference between the current timezone and
    616          UTC. Date/times should always be handled in UTC to avoid confusion but
    617          this is required for legacy systems.
    618      @type timezone_offset: L{datetime.timedelta}
    619
    620      @raise DecodeError: Malformed stream.
    621      @raise RuntimeError: Decoder is unable to fully consume the
    622          stream buffer.
    623
    624      @return: Message envelope.
    625      @rtype: L{Envelope}
    626      """
    627      if not isinstance(stream, util.BufferedByteStream):
    628          stream = util.BufferedByteStream(stream)
    629
    630      if logger is not None:
    631          logger.debug('remoting.decode start')
    632
    633      msg = Envelope()
    634      msg.amfVersion = stream.read_uchar()
    635
    636      # see http://osflash.org/documentation/amf/envelopes/remoting#preamble
    637      # why we are doing this...
    638      if msg.amfVersion > 0x09:
    639          raise pyamf.DecodeError("Malformed stream (amfVersion=%d)" %
    640              msg.amfVersion)
    641
    642      if context is None:
    643          context = pyamf.get_context(pyamf.AMF0, exceptions=False)
    644
    645      decoder = pyamf.get_decoder(pyamf.AMF0, stream, context=context,
    646          strict=strict, timezone_offset=timezone_offset)
    647      msg.clientType = stream.read_uchar()
    648
    649      header_count = stream.read_ushort()
    650
    651      for i in xrange(header_count):
    652          name, required, data = _read_header(stream, decoder, strict)
    653          msg.headers[name] = data
    654
    655          if required:
    656              msg.headers.set_required(name)
    657
    658      body_count = stream.read_short()
    659
    660      for i in range(body_count):
    661          context.clear()
    662
    663          target, payload = _read_body(stream, decoder, strict, logger)
    664          msg[target] = payload
    665
    666      if strict and stream.remaining() > 0:
    667          raise RuntimeError("Unable to fully consume the buffer")
    668
    669      if logger is not None:
    670          logger.debug('remoting.decode end')
    671
    672      return msg
    673
    674
    675 -def encode(msg, context=None, strict=False, logger=None, timezone_offset=None):
    676      """
    677      Encodes AMF stream and returns file object.
    678
    679      @type   msg: L{Envelope}
    680      @param  msg: The message to encode.
    681      @type strict: C{bool}
    682      @param strict: Determines whether encoding should be strict. Specifically
    683          header/body lengths will be written correctly, instead of the default 0.
    684          Default is C{False}. Introduced in 0.4.
    685      @param logger: Used to log interesting events whilst encoding a remoting
    686          message.
    687      @type logger: A L{logging.Logger} instance or C{None}.
    688      @param timezone_offset: The difference between the current timezone and
    689          UTC. Date/times should always be handled in UTC to avoid confusion but
    690          this is required for legacy systems.
    691      @type timezone_offset: L{datetime.timedelta}
    692      @rtype: C{StringIO}
    693      @return: File object.
    694      """
    695      stream = util.BufferedByteStream()
    696
    697      if context is None:
    698          context = pyamf.get_context(pyamf.AMF0, exceptions=False)
    699
    700      encoder = pyamf.get_encoder(pyamf.AMF0, stream, context=context,
    701          timezone_offset=timezone_offset, strict=strict)
    702
    703      if msg.clientType == pyamf.ClientTypes.Flash9:
    704          encoder.use_amf3 = True
    705
    706      stream.write_uchar(msg.amfVersion)
    707      stream.write_uchar(msg.clientType)
    708      stream.write_short(len(msg.headers))
    709
    710      for name, header in msg.headers.iteritems():
    711          _write_header(
    712              name, header, int(msg.headers.is_required(name)),
    713              stream, encoder, strict)
    714
    715      stream.write_short(len(msg))
    716
    717      for name, message in msg.iteritems():
    718          encoder.context.clear()
    719
    720          _write_body(name, message, stream, encoder, strict)
    721
    722      stream.seek(0)
    723
    724      return stream
    725
    726
    727 -def get_exception_from_fault(fault):
    728      """
    729      @raise RemotingError: Default exception from fault.
    730      """
    731      # XXX nick: threading problems here?
    732      try:
    733          return pyamf.ERROR_CLASS_MAP[fault.code]
    734      except KeyError:
    735          # default to RemotingError
    736          return RemotingError
    737
    738
    739  pyamf.register_class(ErrorFault)
    740