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. 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
37 STATUS_OK = 0
38
39 STATUS_ERROR = 1
40
41 STATUS_DEBUG = 2
42
43
44 STATUS_CODES = {
45 STATUS_OK: '/onResult',
46 STATUS_ERROR: '/onStatus',
47 STATUS_DEBUG: '/onDebugEvents'
48 }
49
50
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
63 """
64 Generic remoting error class.
65 """
66
68 """
69 Raised if Server.Call.Failed received
70 """
71
72 pyamf.add_error_class(RemotingCallFailed, ERROR_CODES[ERROR_CALL_FAILED])
73
75 """
76 Collection of AMF message headers.
77 """
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
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
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
106 return len(self.keys())
107
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
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
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
165 for body in self.bodies:
166 if name == body[0]:
167 return body[1]
168
169 raise KeyError("'%r'" % (name,))
170
172 for body in self.bodies:
173 yield body[0], body[1]
174
175 raise StopIteration
176
178 return len(self.bodies)
179
181 for body in self.bodies:
182 yield body
183
184 raise StopIteration
185
187 return [body[0] for body in self.bodies]
188
191
193 for body in self.bodies:
194 if name == body[0]:
195 return True
196
197 return False
198
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
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 """
239 self.envelope = envelope
240 self.body = body
241
244
245 headers = property(_get_headers)
246
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):
258
260 return "<%s target=%s>%s</%s>" % (
261 type(self).__name__, self.target, self.body, type(self).__name__)
262
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 """
274
280
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
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
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
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
331 """
332 I represent an error level fault.
333 """
334 level = 'error'
335
336 pyamf.register_class(ErrorFault)
337
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
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
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
533 raise ValueError("Unknown status code")
534
535 return STATUS_CODES[status]
536
544
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
588
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
679