Package pyamf :: Package remoting
[hide private]
[frames] | no frames]

Source Code for Package pyamf.remoting

  1  # Copyright (c) 2007-2008 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  @author: U{Nick Joyce<mailto:nick@boxdesign.co.uk>} 
 27   
 28  @since: 0.1.0 
 29  """ 
 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(dict):
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 - def __init__(self, amfVersion=None, clientType=None):
115 #: AMF encoding version 116 self.amfVersion = amfVersion 117 #: Client type 118 self.clientType = clientType 119 #: Message headers 120 self.headers = HeaderCollection()
121
122 - def __repr__(self):
123 r = "<Envelope amfVersion=%s clientType=%s>\n" % ( 124 self.amfVersion, self.clientType) 125 126 for h in self.headers: 127 r += " " + repr(h) + "\n" 128 129 for request in iter(self): 130 r += " " + repr(request) + "\n" 131 132 r += "</Envelope>" 133 134 return r
135
136 - def __setitem__(self, idx, value):
137 if not isinstance(value, Message): 138 raise TypeError, "Message instance expected" 139 140 value.envelope = self 141 dict.__setitem__(self, idx, value)
142
143 - def __iter__(self):
144 order = self.keys() 145 order.sort() 146 147 for x in order: 148 yield x, self[x] 149 150 raise StopIteration
151
152 -class Message(object):
153 """ 154 I represent a singular request/response, containing a collection of 155 headers and one body of data. 156 157 I am used to iterate over all requests in the L{Envelope}. 158 159 @ivar envelope: The parent envelope of this AMF Message. 160 @type envelope: L{Envelope} 161 @ivar body: The body of the message. 162 @type body: C{mixed} 163 @ivar headers: The message headers. 164 @type headers: C{dict} 165 """
166 - def __init__(self, envelope, body):
167 self.envelope = envelope 168 self.body = body
169
170 - def _get_headers(self):
171 return self.envelope.headers
172 173 headers = property(_get_headers)
174
175 -class Request(Message):
176 """ 177 An AMF Request payload. 178 179 @ivar target: The target of the request 180 @type target: C{basestring} 181 """
182 - def __init__(self, target, body=[], envelope=None):
183 Message.__init__(self, envelope, body) 184 185 self.target = target
186
187 - def __repr__(self):
188 return "<%s target=%s>%s</%s>" % ( 189 type(self).__name__, self.target, self.body, type(self).__name__)
190
191 -class Response(Message):
192 """ 193 An AMF Response. 194 195 @ivar status: The status of the message. 196 @type status: Member of L{STATUS_CODES} 197 """
198 - def __init__(self, body, status=STATUS_OK, envelope=None):
199 Message.__init__(self, envelope, body) 200 201 self.status = status
202
203 - def __repr__(self):
204 return "<%s status=%s>%s</%s>" % ( 205 type(self).__name__, _get_status(self.status), self.body, 206 type(self).__name__)
207
208 -class BaseFault(object):
209 """ 210 I represent a Fault message (C{mx.rpc.Fault}). 211 212 @ivar level: The level of the fault. 213 @type level: C{str} 214 @ivar code: A simple code describing the fault. 215 @type code: C{str} 216 @ivar details: Any extra details of the fault. 217 @type details: C{str} 218 @ivar description: Text description of the fault. 219 @type description: C{str} 220 221 @see: U{mx.rpc.Fault on Livedocs (external) 222 <http://livedocs.adobe.com/flex/201/langref/mx/rpc/Fault.html>} 223 """ 224 level = None 225
226 - def __init__(self, *args, **kwargs):
227 self.code = kwargs.get('code', '') 228 self.type = kwargs.get('type', '') 229 self.details = kwargs.get('details', '') 230 self.description = kwargs.get('description', '')
231
232 - def __repr__(self):
233 x = '<%s level=%s' % (self.__class__.__name__, self.level) 234 235 if self.code not in ('', None): 236 x += ' code=%s' % self.code 237 if self.type not in ('', None): 238 x += ' type=%s' % self.type 239 if self.description not in ('', None): 240 x += ' description=%s' % self.description 241 242 return x + '>'
243
244 - def raiseException(self):
245 """ 246 Raises an exception based on the fault object. There is no traceback 247 available. 248 """ 249 raise get_exception_from_fault(self), self.description, None
250 251 pyamf.register_class(BaseFault, 252 attrs=['level', 'code', 'type', 'details', 'description']) 253
254 -class ErrorFault(BaseFault):
255 """ 256 I represent an error level fault. 257 """ 258 259 level = 'error'
260 261 pyamf.register_class(ErrorFault) 262
263 -def _read_header(stream, decoder, strict=False):
264 """ 265 Read AMF L{Message} header. 266 267 @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>} 268 @param stream: AMF data. 269 @type decoder: L{amf0.Decoder<pyamf.amf0.Decoder>} 270 @param decoder: AMF decoder instance 271 @type strict: C{bool} 272 @param strict: 273 @raise DecodeError: The data that was read from the stream 274 does not match the header length. 275 276 @rtype: C{tuple} 277 @return: 278 - Name of the header. 279 - A C{bool} determining if understanding this header is 280 required. 281 - Value of the header. 282 """ 283 name_len = stream.read_ushort() 284 name = stream.read_utf8_string(name_len) 285 286 required = bool(stream.read_uchar()) 287 288 data_len = stream.read_ulong() 289 pos = stream.tell() 290 291 data = decoder.readElement() 292 293 if strict and pos + data_len != stream.tell(): 294 raise pyamf.DecodeError( 295 "Data read from stream does not match header length") 296 297 return (name, required, data)
298
299 -def _write_header(name, header, required, stream, encoder, strict=False):
300 """ 301 Write AMF message header. 302 303 @type name: C{str} 304 @param name: Name of the header. 305 @type header: 306 @param header: Raw header data. 307 @type required: L{bool} 308 @param required: Required header. 309 @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>} 310 @param stream: AMF data. 311 @type encoder: L{amf0.Encoder<pyamf.amf0.Encoder>} 312 or L{amf3.Encoder<pyamf.amf3.Encoder>} 313 @param encoder: AMF encoder instance. 314 @type strict: C{bool} 315 @param strict: 316 """ 317 stream.write_ushort(len(name)) 318 stream.write_utf8_string(name) 319 320 stream.write_uchar(required) 321 write_pos = stream.tell() 322 323 stream.write_ulong(0) 324 old_pos = stream.tell() 325 encoder.writeElement(header) 326 new_pos = stream.tell() 327 328 if strict: 329 stream.seek(write_pos) 330 stream.write_ulong(new_pos - old_pos) 331 stream.seek(new_pos)
332
333 -def _read_body(stream, decoder, strict=False):
334 """ 335 Read AMF message body. 336 337 @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>} 338 @param stream: AMF data. 339 @type decoder: L{amf0.Decoder<pyamf.amf0.Decoder>} 340 @param decoder: AMF decoder instance. 341 @type strict: C{bool} 342 @param strict: 343 @raise DecodeError: Data read from stream does not match body length. 344 345 @rtype: C{tuple} 346 @return: A C{tuple} containing: 347 - ID of the request 348 - L{Request} or L{Response} 349 """ 350 def _read_args(): 351 if stream.read(1) != '\x0a': 352 raise pyamf.DecodeError, "Array type required for request body" 353 354 x = stream.read_ulong() 355 356 return [decoder.readElement() for i in xrange(x)]
357 358 target = stream.read_utf8_string(stream.read_ushort()) 359 response = stream.read_utf8_string(stream.read_ushort()) 360 361 status = STATUS_OK 362 is_request = True 363 364 for (code, s) in STATUS_CODES.iteritems(): 365 if target.endswith(s): 366 is_request = False 367 status = code 368 target = target[:0 - len(s)] 369 370 data_len = stream.read_ulong() 371 pos = stream.tell() 372 373 if is_request: 374 data = _read_args() 375 else: 376 data = decoder.readElement() 377 378 if strict and pos + data_len != stream.tell(): 379 raise pyamf.DecodeError("Data read from stream does not match body " 380 "length (%d != %d)" % (pos + data_len, stream.tell(),)) 381 382 if is_request: 383 return (response, Request(target, body=data)) 384 else: 385 if status == STATUS_ERROR and isinstance(data, pyamf.ASObject): 386 data = get_fault(data) 387 388 return (target, Response(data, status)) 389
390 -def _write_body(name, message, stream, encoder, strict=False):
391 """ 392 Write AMF message body. 393 394 @param name: The name of the request. 395 @type name: C{basestring} 396 @param message: The AMF payload. 397 @type message: L{Request} or L{Response} 398 @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>} 399 @type encoder: L{amf0.Encoder<pyamf.amf0.Encoder>} 400 @param encoder: Encoder to use. 401 @type strict: C{bool} 402 @param strict: Use strict encoding policy. 403 """ 404 if not isinstance(message, (Request, Response)): 405 raise TypeError, "Unknown message type" 406 407 target = None 408 409 if isinstance(message, Request): 410 target = unicode(message.target) 411 else: 412 target = u"%s%s" % (name, _get_status(message.status)) 413 414 target = target.encode('utf8') 415 416 stream.write_ushort(len(target)) 417 stream.write_utf8_string(target) 418 419 response = 'null' 420 421 if isinstance(message, Request): 422 response = name 423 424 stream.write_ushort(len(response)) 425 stream.write_utf8_string(response) 426 427 if not strict: 428 stream.write_ulong(0) 429 encoder.writeElement(message.body) 430 else: 431 write_pos = stream.tell() 432 stream.write_ulong(0) 433 old_pos = stream.tell() 434 435 encoder.writeElement(message.body) 436 new_pos = stream.tell() 437 438 stream.seek(write_pos) 439 stream.write_ulong(new_pos - old_pos) 440 stream.seek(new_pos)
441
442 -def _get_status(status):
443 """ 444 Get status code. 445 446 @type status: C{str} 447 @raise ValueError: The status code is unknown. 448 @return: Status code. 449 """ 450 if status not in STATUS_CODES.keys(): 451 # TODO print that status code.. 452 raise ValueError, "Unknown status code" 453 454 return STATUS_CODES[status]
455
456 -def get_fault_class(level, **kwargs):
457 code = kwargs.get('code', '') 458 459 if level == 'error': 460 return ErrorFault 461 462 return BaseFault
463
464 -def get_fault(data):
465 try: 466 level = data['level'] 467 del data['level'] 468 except KeyError: 469 level = 'error' 470 471 e = {} 472 473 for x, y in data.iteritems(): 474 if isinstance(x, unicode): 475 e[str(x)] = y 476 else: 477 e[x] = y 478 479 return get_fault_class(level, **e)(**e)
480
481 -def decode(stream, context=None, strict=False):
482 """ 483 Decodes the incoming stream. . 484 485 @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>} 486 @param stream: AMF data. 487 @type context: L{amf0.Context<pyamf.amf0.Context>} or 488 L{amf3.Context<pyamf.amf3.Context>} 489 @param context: Context. 490 @type strict: C{bool} 491 @param strict: Enforce strict encoding. 492 493 @raise DecodeError: Malformed stream. 494 @raise RuntimeError: Decoder is unable to fully consume the 495 stream buffer. 496 497 @return: Message envelope. 498 @rtype: L{Envelope} 499 """ 500 if not isinstance(stream, util.BufferedByteStream): 501 stream = util.BufferedByteStream(stream) 502 503 msg = Envelope() 504 505 msg.amfVersion = stream.read_uchar() 506 507 # see http://osflash.org/documentation/amf/envelopes/remoting#preamble 508 # why we are doing this... 509 if msg.amfVersion > 0x09: 510 raise pyamf.DecodeError("Malformed stream (amfVersion=%d)" % 511 msg.amfVersion) 512 513 if context is None: 514 context = pyamf.get_context(pyamf.AMF0) 515 516 decoder = pyamf._get_decoder_class(pyamf.AMF0)(stream, context=context) 517 msg.clientType = stream.read_uchar() 518 519 header_count = stream.read_ushort() 520 521 for i in xrange(header_count): 522 name, required, data = _read_header(stream, decoder, strict) 523 msg.headers[name] = data 524 525 if required: 526 msg.headers.set_required(name) 527 528 body_count = stream.read_short() 529 context.clear() 530 531 for i in range(body_count): 532 target, payload = _read_body(stream, decoder, strict) 533 msg[target] = payload 534 535 if strict and stream.remaining() > 0: 536 raise RuntimeError, "Unable to fully consume the buffer" 537 538 return msg
539
540 -def encode(msg, old_context=None, strict=False):
541 """ 542 Encodes AMF stream and returns file object. 543 544 @type msg: L{Envelope} 545 @param msg: The message to encode. 546 @type old_context: L{amf0.Context<pyamf.amf0.Context>} or 547 L{amf3.Context<pyamf.amf3.Context>} 548 @param old_context: Context. 549 @type strict: C{bool} 550 @param strict: Determines whether encoding should be strict. Specifically 551 header/body lengths will be written correctly, instead of the default 0. 552 553 @rtype: C{StringIO} 554 @return: File object. 555 """ 556 def getNewContext(): 557 if old_context: 558 import copy 559 560 return copy.copy(old_context) 561 else: 562 return pyamf.get_context(pyamf.AMF0)
563 564 stream = util.BufferedByteStream() 565 566 encoder = pyamf._get_encoder_class(msg.amfVersion)(stream) 567 568 stream.write_uchar(msg.amfVersion) 569 stream.write_uchar(msg.clientType) 570 stream.write_short(len(msg.headers)) 571 572 for name, header in msg.headers.iteritems(): 573 _write_header( 574 name, header, msg.headers.is_required(name), 575 stream, encoder, strict) 576 577 stream.write_short(len(msg)) 578 579 for name, message in msg.iteritems(): 580 # Each body requires a new context 581 encoder.context = getNewContext() 582 _write_body(name, message, stream, encoder, strict) 583 584 return stream 585
586 -def get_exception_from_fault(fault):
587 # XXX nick: threading problems here? 588 try: 589 return pyamf.ERROR_CLASS_MAP[fault.code] 590 except KeyError: 591 # default to RemotingError 592 return RemotingError
593