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 @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
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 - def __init__(self, amfVersion=None, clientType=None):
115
116 self.amfVersion = amfVersion
117
118 self.clientType = clientType
119
120 self.headers = HeaderCollection()
121
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
137 if not isinstance(value, Message):
138 raise TypeError, "Message instance expected"
139
140 value.envelope = self
141 dict.__setitem__(self, idx, value)
142
144 order = self.keys()
145 order.sort()
146
147 for x in order:
148 yield x, self[x]
149
150 raise StopIteration
151
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 """
167 self.envelope = envelope
168 self.body = body
169
172
173 headers = property(_get_headers)
174
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):
186
188 return "<%s target=%s>%s</%s>" % (
189 type(self).__name__, self.target, self.body, type(self).__name__)
190
192 """
193 An AMF Response.
194
195 @ivar status: The status of the message.
196 @type status: Member of L{STATUS_CODES}
197 """
202
207
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
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
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
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
255 """
256 I represent an error level fault.
257 """
258
259 level = 'error'
260
261 pyamf.register_class(ErrorFault)
262
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
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
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
452 raise ValueError, "Unknown status code"
453
454 return STATUS_CODES[status]
455
463
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
508
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
581 encoder.context = getNewContext()
582 _write_body(name, message, stream, encoder, strict)
583
584 return stream
585
593