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