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 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__, self.target, 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), 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' % self.code 310 if self.type not in ('', None): 311 x += ' type=%s' % self.type 312 if self.description not in ('', None): 313 x += ' description=%s' % self.description 314 315 if self.details not in ('', None): 316 x += '\nTraceback:\n%s' % (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 # XXX nick: threading problems here? 674 try: 675 return pyamf.ERROR_CLASS_MAP[fault.code] 676 except KeyError: 677 # default to RemotingError 678 return RemotingError
679