1
2
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
35 STATUS_OK = 0
36
37 STATUS_ERROR = 1
38
39 STATUS_DEBUG = 2
40
41
42 STATUS_CODES = {
43 STATUS_OK: '/onResult',
44 STATUS_ERROR: '/onStatus',
45 STATUS_DEBUG: '/onDebugEvents'
46 }
47
48
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
62 """
63 Generic remoting error class.
64 """
65
66
68 """
69 Raised if C{Server.Call.Failed} received.
70 """
71
72 pyamf.add_error_class(RemotingCallFailed, ERROR_CODES[ERROR_CALL_FAILED])
73
74
76 """
77 Collection of AMF message headers.
78 """
79
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
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
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
108 return len(self.keys())
109
110
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
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
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
169 for body in self.bodies:
170 if name == body[0]:
171 return body[1]
172
173 raise KeyError("'%r'" % (name,))
174
176 for body in self.bodies:
177 yield body[0], body[1]
178
179 raise StopIteration
180
182 return len(self.bodies)
183
185 for body in self.bodies:
186 yield body
187
188 raise StopIteration
189
191 return [body[0] for body in self.bodies]
192
195
197 for body in self.bodies:
198 if name == body[0]:
199 return True
200
201 return False
202
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
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
245 self.envelope = envelope
246 self.body = body
247
250
251 headers = property(_get_headers)
252
253
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):
266
268 return "<%s target=%s>%s</%s>" % (
269 type(self).__name__, repr(self.target), repr(self.body), type(self).__name__)
270
271
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
284
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
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
312 static = ('level', 'code', 'type', 'details', 'description')
313
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
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
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
344 """
345 I represent an error level fault.
346 """
347
348 level = 'error'
349
350
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
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
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
571 raise ValueError("Unknown status code")
572
573 return STATUS_CODES[status]
574
575
581
582
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
637
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
728 """
729 @raise RemotingError: Default exception from fault.
730 """
731
732 try:
733 return pyamf.ERROR_CLASS_MAP[fault.code]
734 except KeyError:
735
736 return RemotingError
737
738
739 pyamf.register_class(ErrorFault)
740