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