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 def _encode_body(message): 485 if isinstance(message, Response): 486 encoder.writeElement(message.body) 487 488 return 489 490 stream.write('\x0a') 491 stream.write_ulong(len(message.body)) 492 for x in message.body: 493 encoder.writeElement(x)
494 495 if not isinstance(message, (Request, Response)): 496 raise TypeError("Unknown message type") 497 498 target = None 499 500 if isinstance(message, Request): 501 target = unicode(message.target) 502 else: 503 target = u"%s%s" % (name, _get_status(message.status)) 504 505 target = target.encode('utf8') 506 507 stream.write_ushort(len(target)) 508 stream.write_utf8_string(target) 509 510 response = 'null' 511 512 if isinstance(message, Request): 513 response = name 514 515 stream.write_ushort(len(response)) 516 stream.write_utf8_string(response) 517 518 if not strict: 519 stream.write_ulong(0) 520 _encode_body(message) 521 else: 522 write_pos = stream.tell() 523 stream.write_ulong(0) 524 old_pos = stream.tell() 525 526 _encode_body(message) 527 new_pos = stream.tell() 528 529 stream.seek(write_pos) 530 stream.write_ulong(new_pos - old_pos) 531 stream.seek(new_pos) 532
533 -def _get_status(status):
534 """ 535 Get status code. 536 537 @type status: C{str} 538 @raise ValueError: The status code is unknown. 539 @return: Status code. 540 @see: L{STATUS_CODES} 541 """ 542 if status not in STATUS_CODES.keys(): 543 # TODO print that status code.. 544 raise ValueError("Unknown status code") 545 546 return STATUS_CODES[status]
547
548 -def get_fault_class(level, **kwargs):
549 code = kwargs.get('code', '') 550 551 if level == 'error': 552 return ErrorFault 553 554 return BaseFault
555
556 -def get_fault(data):
557 try: 558 level = data['level'] 559 del data['level'] 560 except KeyError: 561 level = 'error' 562 563 e = {} 564 565 for x, y in data.iteritems(): 566 if isinstance(x, unicode): 567 e[str(x)] = y 568 else: 569 e[x] = y 570 571 return get_fault_class(level, **e)(**e)
572
573 -def decode(stream, context=None, strict=False):
574 """ 575 Decodes the incoming stream. 576 577 @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>} 578 @param stream: AMF data. 579 @type context: L{amf0.Context<pyamf.amf0.Context>} or 580 L{amf3.Context<pyamf.amf3.Context>} 581 @param context: Context. 582 @type strict: C{bool} 583 @param strict: Enforce strict encoding. Default is C{False}. 584 585 @raise DecodeError: Malformed stream. 586 @raise RuntimeError: Decoder is unable to fully consume the 587 stream buffer. 588 589 @return: Message envelope. 590 @rtype: L{Envelope} 591 """ 592 if not isinstance(stream, util.BufferedByteStream): 593 stream = util.BufferedByteStream(stream) 594 595 msg = Envelope() 596 msg.amfVersion = stream.read_uchar() 597 598 # see http://osflash.org/documentation/amf/envelopes/remoting#preamble 599 # why we are doing this... 600 if msg.amfVersion > 0x09: 601 raise pyamf.DecodeError("Malformed stream (amfVersion=%d)" % 602 msg.amfVersion) 603 604 if context is None: 605 context = pyamf.get_context(pyamf.AMF0) 606 else: 607 context = copy.copy(context) 608 609 decoder = pyamf._get_decoder_class(pyamf.AMF0)(stream, context=context, strict=strict) 610 msg.clientType = stream.read_uchar() 611 612 header_count = stream.read_ushort() 613 614 for i in xrange(header_count): 615 name, required, data = _read_header(stream, decoder, strict) 616 msg.headers[name] = data 617 618 if required: 619 msg.headers.set_required(name) 620 621 body_count = stream.read_short() 622 623 for i in range(body_count): 624 context.reset() 625 626 target, payload = _read_body(stream, decoder, strict) 627 msg[target] = payload 628 629 if strict and stream.remaining() > 0: 630 raise RuntimeError("Unable to fully consume the buffer") 631 632 return msg
633
634 -def encode(msg, context=None, strict=False):
635 """ 636 Encodes AMF stream and returns file object. 637 638 @type msg: L{Envelope} 639 @param msg: The message to encode. 640 @type context: L{amf0.Context<pyamf.amf0.Context>} or 641 L{amf3.Context<pyamf.amf3.Context>} 642 @param context: Context. 643 @type strict: C{bool} 644 @param strict: Determines whether encoding should be strict. Specifically 645 header/body lengths will be written correctly, instead of the default 0. 646 Default is C{False}. Introduced in 0.4. 647 @rtype: C{StringIO} 648 @return: File object. 649 """ 650 def getNewContext(): 651 if context: 652 new_context = copy.copy(context) 653 new_context.reset() 654 655 return new_context 656 else: 657 return pyamf.get_context(pyamf.AMF0)
658 659 stream = util.BufferedByteStream() 660 encoder = pyamf._get_encoder_class(pyamf.AMF0)(stream, strict=strict) 661 662 if msg.clientType == pyamf.ClientTypes.Flash9: 663 encoder.use_amf3 = True 664 665 stream.write_uchar(msg.amfVersion) 666 stream.write_uchar(msg.clientType) 667 stream.write_short(len(msg.headers)) 668 669 for name, header in msg.headers.iteritems(): 670 _write_header( 671 name, header, msg.headers.is_required(name), 672 stream, encoder, strict) 673 674 stream.write_short(len(msg)) 675 676 for name, message in msg.iteritems(): 677 encoder.context = getNewContext() 678 679 _write_body(name, message, stream, encoder, strict) 680 681 return stream 682
683 -def get_exception_from_fault(fault):
684 """ 685 @raise RemotingError: Default exception from fault. 686 """ 687 # XXX nick: threading problems here? 688 try: 689 return pyamf.ERROR_CLASS_MAP[fault.code] 690 except KeyError: 691 # default to RemotingError 692 return RemotingError
693