| # Copyright (c) The PyAMF Project. | |
| # See LICENSE.txt for details. | |
| """ | |
| AMF0 implementation. | |
| C{AMF0} supports the basic data types used for the NetConnection, NetStream, | |
| LocalConnection, SharedObjects and other classes in the Adobe Flash Player. | |
| @since: 0.1 | |
| @see: U{Official AMF0 Specification in English (external) | |
| <http://opensource.adobe.com/wiki/download/attachments/1114283/amf0_spec_121207.pdf>} | |
| @see: U{Official AMF0 Specification in Japanese (external) | |
| <http://opensource.adobe.com/wiki/download/attachments/1114283/JP_amf0_spec_121207.pdf>} | |
| @see: U{AMF documentation on OSFlash (external) | |
| <http://osflash.org/documentation/amf>} | |
| """ | |
| import datetime | |
| import pyamf | |
| from pyamf import util, codec, xml, python | |
| #: Represented as 9 bytes: 1 byte for C{0x00} and 8 bytes a double | |
| #: representing the value of the number. | |
| TYPE_NUMBER = '\x00' | |
| #: Represented as 2 bytes: 1 byte for C{0x01} and a second, C{0x00} | |
| #: for C{False}, C{0x01} for C{True}. | |
| TYPE_BOOL = '\x01' | |
| #: Represented as 3 bytes + len(String): 1 byte C{0x02}, then a UTF8 string, | |
| #: including the top two bytes representing string length as a C{int}. | |
| TYPE_STRING = '\x02' | |
| #: Represented as 1 byte, C{0x03}, then pairs of UTF8 string, the key, and | |
| #: an AMF element, ended by three bytes, C{0x00} C{0x00} C{0x09}. | |
| TYPE_OBJECT = '\x03' | |
| #: MovieClip does not seem to be supported by Remoting. | |
| #: It may be used by other AMF clients such as SharedObjects. | |
| TYPE_MOVIECLIP = '\x04' | |
| #: 1 single byte, C{0x05} indicates null. | |
| TYPE_NULL = '\x05' | |
| #: 1 single byte, C{0x06} indicates null. | |
| TYPE_UNDEFINED = '\x06' | |
| #: When an ActionScript object refers to itself, such C{this.self = this}, | |
| #: or when objects are repeated within the same scope (for example, as the | |
| #: two parameters of the same function called), a code of C{0x07} and an | |
| #: C{int}, the reference number, are written. | |
| TYPE_REFERENCE = '\x07' | |
| #: A MixedArray is indicated by code C{0x08}, then a Long representing the | |
| #: highest numeric index in the array, or 0 if there are none or they are | |
| #: all negative. After that follow the elements in key : value pairs. | |
| TYPE_MIXEDARRAY = '\x08' | |
| #: @see: L{TYPE_OBJECT} | |
| TYPE_OBJECTTERM = '\x09' | |
| #: An array is indicated by C{0x0A}, then a Long for array length, then the | |
| #: array elements themselves. Arrays are always sparse; values for | |
| #: inexistant keys are set to null (C{0x06}) to maintain sparsity. | |
| TYPE_ARRAY = '\x0A' | |
| #: Date is represented as C{0x0B}, then a double, then an C{int}. The double | |
| #: represents the number of milliseconds since 01/01/1970. The C{int} represents | |
| #: the timezone offset in minutes between GMT. Note for the latter than values | |
| #: greater than 720 (12 hours) are represented as M{2^16} - the value. Thus GMT+1 | |
| #: is 60 while GMT-5 is 65236. | |
| TYPE_DATE = '\x0B' | |
| #: LongString is reserved for strings larger then M{2^16} characters long. It | |
| #: is represented as C{0x0C} then a LongUTF. | |
| TYPE_LONGSTRING = '\x0C' | |
| #: Trying to send values which don't make sense, such as prototypes, functions, | |
| #: built-in objects, etc. will be indicated by a single C{00x0D} byte. | |
| TYPE_UNSUPPORTED = '\x0D' | |
| #: Remoting Server -> Client only. | |
| #: @see: L{RecordSet} | |
| #: @see: U{RecordSet structure on OSFlash | |
| #: <http://osflash.org/documentation/amf/recordset>} | |
| TYPE_RECORDSET = '\x0E' | |
| #: The XML element is indicated by C{0x0F} and followed by a LongUTF containing | |
| #: the string representation of the XML object. The receiving gateway may which | |
| #: to wrap this string inside a language-specific standard XML object, or simply | |
| #: pass as a string. | |
| TYPE_XML = '\x0F' | |
| #: A typed object is indicated by C{0x10}, then a UTF string indicating class | |
| #: name, and then the same structure as a normal C{0x03} Object. The receiving | |
| #: gateway may use a mapping scheme, or send back as a vanilla object or | |
| #: associative array. | |
| TYPE_TYPEDOBJECT = '\x10' | |
| #: An AMF message sent from an AVM+ client such as the Flash Player 9 may break | |
| #: out into L{AMF3<pyamf.amf3>} mode. In this case the next byte will be the | |
| #: AMF3 type code and the data will be in AMF3 format until the decoded object | |
| #: reaches it's logical conclusion (for example, an object has no more keys). | |
| TYPE_AMF3 = '\x11' | |
| class Context(codec.Context): | |
| """ | |
| """ | |
| def clear(self): | |
| codec.Context.clear(self) | |
| encoder = self.extra.get('amf3_encoder', None) | |
| if encoder: | |
| encoder.context.clear() | |
| decoder = self.extra.get('amf3_decoder', None) | |
| if decoder: | |
| decoder.context.clear() | |
| def getAMF3Encoder(self, amf0_encoder): | |
| encoder = self.extra.get('amf3_encoder', None) | |
| if encoder: | |
| return encoder | |
| encoder = pyamf.get_encoder(pyamf.AMF3, stream=amf0_encoder.stream, | |
| timezone_offset=amf0_encoder.timezone_offset) | |
| self.extra['amf3_encoder'] = encoder | |
| return encoder | |
| def getAMF3Decoder(self, amf0_decoder): | |
| decoder = self.extra.get('amf3_decoder', None) | |
| if decoder: | |
| return decoder | |
| decoder = pyamf.get_decoder(pyamf.AMF3, stream=amf0_decoder.stream, | |
| timezone_offset=amf0_decoder.timezone_offset) | |
| self.extra['amf3_decoder'] = decoder | |
| return decoder | |
| class Decoder(codec.Decoder): | |
| """ | |
| Decodes an AMF0 stream. | |
| """ | |
| def buildContext(self): | |
| return Context() | |
| def getTypeFunc(self, data): | |
| # great for coverage, sucks for readability | |
| if data == TYPE_NUMBER: | |
| return self.readNumber | |
| elif data == TYPE_BOOL: | |
| return self.readBoolean | |
| elif data == TYPE_STRING: | |
| return self.readString | |
| elif data == TYPE_OBJECT: | |
| return self.readObject | |
| elif data == TYPE_NULL: | |
| return self.readNull | |
| elif data == TYPE_UNDEFINED: | |
| return self.readUndefined | |
| elif data == TYPE_REFERENCE: | |
| return self.readReference | |
| elif data == TYPE_MIXEDARRAY: | |
| return self.readMixedArray | |
| elif data == TYPE_ARRAY: | |
| return self.readList | |
| elif data == TYPE_DATE: | |
| return self.readDate | |
| elif data == TYPE_LONGSTRING: | |
| return self.readLongString | |
| elif data == TYPE_UNSUPPORTED: | |
| return self.readNull | |
| elif data == TYPE_XML: | |
| return self.readXML | |
| elif data == TYPE_TYPEDOBJECT: | |
| return self.readTypedObject | |
| elif data == TYPE_AMF3: | |
| return self.readAMF3 | |
| def readNumber(self): | |
| """ | |
| Reads a ActionScript C{Number} value. | |
| In ActionScript 1 and 2 the C{NumberASTypes} type represents all numbers, | |
| both floats and integers. | |
| @rtype: C{int} or C{float} | |
| """ | |
| return _check_for_int(self.stream.read_double()) | |
| def readBoolean(self): | |
| """ | |
| Reads a ActionScript C{Boolean} value. | |
| @rtype: C{bool} | |
| @return: Boolean. | |
| """ | |
| return bool(self.stream.read_uchar()) | |
| def readString(self, bytes=False): | |
| """ | |
| Reads a C{string} from the stream. If bytes is C{True} then you will get | |
| the raw data read from the stream, otherwise a string that has been | |
| B{utf-8} decoded. | |
| """ | |
| l = self.stream.read_ushort() | |
| b = self.stream.read(l) | |
| if bytes: | |
| return b | |
| return self.context.getStringForBytes(b) | |
| def readNull(self): | |
| """ | |
| Reads a ActionScript C{null} value. | |
| """ | |
| return None | |
| def readUndefined(self): | |
| """ | |
| Reads an ActionScript C{undefined} value. | |
| @return: L{Undefined<pyamf.Undefined>} | |
| """ | |
| return pyamf.Undefined | |
| def readMixedArray(self): | |
| """ | |
| Read mixed array. | |
| @rtype: L{pyamf.MixedArray} | |
| """ | |
| # TODO: something with the length/strict | |
| self.stream.read_ulong() # length | |
| obj = pyamf.MixedArray() | |
| self.context.addObject(obj) | |
| attrs = self.readObjectAttributes(obj) | |
| for key in attrs.keys(): | |
| try: | |
| key = int(key) | |
| except ValueError: | |
| pass | |
| obj[key] = attrs[key] | |
| return obj | |
| def readList(self): | |
| """ | |
| Read a C{list} from the data stream. | |
| """ | |
| obj = [] | |
| self.context.addObject(obj) | |
| l = self.stream.read_ulong() | |
| for i in xrange(l): | |
| obj.append(self.readElement()) | |
| return obj | |
| def readTypedObject(self): | |
| """ | |
| Reads an aliased ActionScript object from the stream and attempts to | |
| 'cast' it into a python class. | |
| @see: L{pyamf.register_class} | |
| """ | |
| class_alias = self.readString() | |
| try: | |
| alias = self.context.getClassAlias(class_alias) | |
| except pyamf.UnknownClassAlias: | |
| if self.strict: | |
| raise | |
| alias = pyamf.TypedObjectClassAlias(class_alias) | |
| obj = alias.createInstance(codec=self) | |
| self.context.addObject(obj) | |
| attrs = self.readObjectAttributes(obj) | |
| alias.applyAttributes(obj, attrs, codec=self) | |
| return obj | |
| def readAMF3(self): | |
| """ | |
| Read AMF3 elements from the data stream. | |
| @return: The AMF3 element read from the stream | |
| """ | |
| return self.context.getAMF3Decoder(self).readElement() | |
| def readObjectAttributes(self, obj): | |
| obj_attrs = {} | |
| key = self.readString(True) | |
| while self.stream.peek() != TYPE_OBJECTTERM: | |
| obj_attrs[key] = self.readElement() | |
| key = self.readString(True) | |
| # discard the end marker (TYPE_OBJECTTERM) | |
| self.stream.read(1) | |
| return obj_attrs | |
| def readObject(self): | |
| """ | |
| Reads an anonymous object from the data stream. | |
| @rtype: L{ASObject<pyamf.ASObject>} | |
| """ | |
| obj = pyamf.ASObject() | |
| self.context.addObject(obj) | |
| obj.update(self.readObjectAttributes(obj)) | |
| return obj | |
| def readReference(self): | |
| """ | |
| Reads a reference from the data stream. | |
| @raise pyamf.ReferenceError: Unknown reference. | |
| """ | |
| idx = self.stream.read_ushort() | |
| o = self.context.getObject(idx) | |
| if o is None: | |
| raise pyamf.ReferenceError('Unknown reference %d' % (idx,)) | |
| return o | |
| def readDate(self): | |
| """ | |
| Reads a UTC date from the data stream. Client and servers are | |
| responsible for applying their own timezones. | |
| Date: C{0x0B T7 T6} .. C{T0 Z1 Z2 T7} to C{T0} form a 64 bit | |
| Big Endian number that specifies the number of nanoseconds | |
| that have passed since 1/1/1970 0:00 to the specified time. | |
| This format is UTC 1970. C{Z1} and C{Z0} for a 16 bit Big | |
| Endian number indicating the indicated time's timezone in | |
| minutes. | |
| """ | |
| ms = self.stream.read_double() / 1000.0 | |
| self.stream.read_short() # tz | |
| # Timezones are ignored | |
| d = util.get_datetime(ms) | |
| if self.timezone_offset: | |
| d = d + self.timezone_offset | |
| self.context.addObject(d) | |
| return d | |
| def readLongString(self): | |
| """ | |
| Read UTF8 string. | |
| """ | |
| l = self.stream.read_ulong() | |
| bytes = self.stream.read(l) | |
| return self.context.getStringForBytes(bytes) | |
| def readXML(self): | |
| """ | |
| Read XML. | |
| """ | |
| data = self.readLongString() | |
| root = xml.fromstring(data) | |
| self.context.addObject(root) | |
| return root | |
| class Encoder(codec.Encoder): | |
| """ | |
| Encodes an AMF0 stream. | |
| @ivar use_amf3: A flag to determine whether this encoder should default to | |
| using AMF3. Defaults to C{False} | |
| @type use_amf3: C{bool} | |
| """ | |
| def __init__(self, *args, **kwargs): | |
| codec.Encoder.__init__(self, *args, **kwargs) | |
| self.use_amf3 = kwargs.pop('use_amf3', False) | |
| def buildContext(self): | |
| return Context() | |
| def getTypeFunc(self, data): | |
| if self.use_amf3: | |
| return self.writeAMF3 | |
| t = type(data) | |
| if t is pyamf.MixedArray: | |
| return self.writeMixedArray | |
| return codec.Encoder.getTypeFunc(self, data) | |
| def writeType(self, t): | |
| """ | |
| Writes the type to the stream. | |
| @type t: C{str} | |
| @param t: ActionScript type. | |
| """ | |
| self.stream.write(t) | |
| def writeUndefined(self, data): | |
| """ | |
| Writes the L{undefined<TYPE_UNDEFINED>} data type to the stream. | |
| @param data: Ignored, here for the sake of interface. | |
| """ | |
| self.writeType(TYPE_UNDEFINED) | |
| def writeNull(self, n): | |
| """ | |
| Write null type to data stream. | |
| """ | |
| self.writeType(TYPE_NULL) | |
| def writeList(self, a): | |
| """ | |
| Write array to the stream. | |
| @param a: The array data to be encoded to the AMF0 data stream. | |
| """ | |
| if self.writeReference(a) != -1: | |
| return | |
| self.context.addObject(a) | |
| self.writeType(TYPE_ARRAY) | |
| self.stream.write_ulong(len(a)) | |
| for data in a: | |
| self.writeElement(data) | |
| def writeNumber(self, n): | |
| """ | |
| Write number to the data stream . | |
| @param n: The number data to be encoded to the AMF0 data stream. | |
| """ | |
| self.writeType(TYPE_NUMBER) | |
| self.stream.write_double(float(n)) | |
| def writeBoolean(self, b): | |
| """ | |
| Write boolean to the data stream. | |
| @param b: The boolean data to be encoded to the AMF0 data stream. | |
| """ | |
| self.writeType(TYPE_BOOL) | |
| if b: | |
| self.stream.write_uchar(1) | |
| else: | |
| self.stream.write_uchar(0) | |
| def serialiseString(self, s): | |
| """ | |
| Similar to L{writeString} but does not encode a type byte. | |
| """ | |
| if type(s) is unicode: | |
| s = self.context.getBytesForString(s) | |
| l = len(s) | |
| if l > 0xffff: | |
| self.stream.write_ulong(l) | |
| else: | |
| self.stream.write_ushort(l) | |
| self.stream.write(s) | |
| def writeBytes(self, s): | |
| """ | |
| Write a string of bytes to the data stream. | |
| """ | |
| l = len(s) | |
| if l > 0xffff: | |
| self.writeType(TYPE_LONGSTRING) | |
| else: | |
| self.writeType(TYPE_STRING) | |
| if l > 0xffff: | |
| self.stream.write_ulong(l) | |
| else: | |
| self.stream.write_ushort(l) | |
| self.stream.write(s) | |
| def writeString(self, u): | |
| """ | |
| Write a unicode to the data stream. | |
| """ | |
| s = self.context.getBytesForString(u) | |
| self.writeBytes(s) | |
| def writeReference(self, o): | |
| """ | |
| Write reference to the data stream. | |
| @param o: The reference data to be encoded to the AMF0 datastream. | |
| """ | |
| idx = self.context.getObjectReference(o) | |
| if idx == -1 or idx > 65535: | |
| return -1 | |
| self.writeType(TYPE_REFERENCE) | |
| self.stream.write_ushort(idx) | |
| return idx | |
| def _writeDict(self, o): | |
| """ | |
| Write C{dict} to the data stream. | |
| @param o: The C{dict} data to be encoded to the AMF0 data stream. | |
| """ | |
| for key, val in o.iteritems(): | |
| if type(key) in python.int_types: | |
| key = str(key) | |
| self.serialiseString(key) | |
| self.writeElement(val) | |
| def writeMixedArray(self, o): | |
| """ | |
| Write mixed array to the data stream. | |
| @type o: L{pyamf.MixedArray} | |
| """ | |
| if self.writeReference(o) != -1: | |
| return | |
| self.context.addObject(o) | |
| self.writeType(TYPE_MIXEDARRAY) | |
| # TODO: optimise this | |
| # work out the highest integer index | |
| try: | |
| # list comprehensions to save the day | |
| max_index = max([y[0] for y in o.items() | |
| if isinstance(y[0], (int, long))]) | |
| if max_index < 0: | |
| max_index = 0 | |
| except ValueError: | |
| max_index = 0 | |
| self.stream.write_ulong(max_index) | |
| self._writeDict(o) | |
| self._writeEndObject() | |
| def _writeEndObject(self): | |
| self.stream.write('\x00\x00' + TYPE_OBJECTTERM) | |
| def writeObject(self, o): | |
| """ | |
| Write a Python object to the stream. | |
| @param o: The object data to be encoded to the AMF0 data stream. | |
| """ | |
| if self.writeReference(o) != -1: | |
| return | |
| self.context.addObject(o) | |
| alias = self.context.getClassAlias(o.__class__) | |
| alias.compile() | |
| if alias.amf3: | |
| self.writeAMF3(o) | |
| return | |
| if alias.anonymous: | |
| self.writeType(TYPE_OBJECT) | |
| else: | |
| self.writeType(TYPE_TYPEDOBJECT) | |
| self.serialiseString(alias.alias) | |
| attrs = alias.getEncodableAttributes(o, codec=self) | |
| if alias.static_attrs and attrs: | |
| for key in alias.static_attrs: | |
| value = attrs.pop(key) | |
| self.serialiseString(key) | |
| self.writeElement(value) | |
| if attrs: | |
| self._writeDict(attrs) | |
| self._writeEndObject() | |
| def writeDate(self, d): | |
| """ | |
| Writes a date to the data stream. | |
| @type d: Instance of C{datetime.datetime} | |
| @param d: The date to be encoded to the AMF0 data stream. | |
| """ | |
| if isinstance(d, datetime.time): | |
| raise pyamf.EncodeError('A datetime.time instance was found but ' | |
| 'AMF0 has no way to encode time objects. Please use ' | |
| 'datetime.datetime instead (got:%r)' % (d,)) | |
| # According to the Red5 implementation of AMF0, dates references are | |
| # created, but not used. | |
| if self.timezone_offset is not None: | |
| d -= self.timezone_offset | |
| secs = util.get_timestamp(d) | |
| tz = 0 | |
| self.writeType(TYPE_DATE) | |
| self.stream.write_double(secs * 1000.0) | |
| self.stream.write_short(tz) | |
| def writeXML(self, e): | |
| """ | |
| Writes an XML instance. | |
| """ | |
| self.writeType(TYPE_XML) | |
| data = xml.tostring(e) | |
| if isinstance(data, unicode): | |
| data = data.encode('utf-8') | |
| self.stream.write_ulong(len(data)) | |
| self.stream.write(data) | |
| def writeAMF3(self, data): | |
| """ | |
| Writes an element in L{AMF3<pyamf.amf3>} format. | |
| """ | |
| self.writeType(TYPE_AMF3) | |
| self.context.getAMF3Encoder(self).writeElement(data) | |
| class RecordSet(object): | |
| """ | |
| I represent the C{RecordSet} class used in Adobe Flash Remoting to hold | |
| (amongst other things) SQL records. | |
| @ivar columns: The columns to send. | |
| @type columns: List of strings. | |
| @ivar items: The C{RecordSet} data. | |
| @type items: List of lists, the order of the data corresponds to the order | |
| of the columns. | |
| @ivar service: Service linked to the C{RecordSet}. | |
| @type service: | |
| @ivar id: The id of the C{RecordSet}. | |
| @type id: C{str} | |
| @see: U{RecordSet on OSFlash (external) | |
| <http://osflash.org/documentation/amf/recordset>} | |
| """ | |
| class __amf__: | |
| alias = 'RecordSet' | |
| static = ('serverInfo',) | |
| dynamic = False | |
| def __init__(self, columns=[], items=[], service=None, id=None): | |
| self.columns = columns | |
| self.items = items | |
| self.service = service | |
| self.id = id | |
| def _get_server_info(self): | |
| ret = pyamf.ASObject(totalCount=len(self.items), cursor=1, version=1, | |
| initialData=self.items, columnNames=self.columns) | |
| if self.service is not None: | |
| ret.update({'serviceName': str(self.service['name'])}) | |
| if self.id is not None: | |
| ret.update({'id':str(self.id)}) | |
| return ret | |
| def _set_server_info(self, val): | |
| self.columns = val['columnNames'] | |
| self.items = val['initialData'] | |
| try: | |
| # TODO nick: find relevant service and link in here. | |
| self.service = dict(name=val['serviceName']) | |
| except KeyError: | |
| self.service = None | |
| try: | |
| self.id = val['id'] | |
| except KeyError: | |
| self.id = None | |
| serverInfo = property(_get_server_info, _set_server_info) | |
| def __repr__(self): | |
| ret = '<%s.%s' % (self.__module__, self.__class__.__name__) | |
| if self.id is not None: | |
| ret += ' id=%s' % self.id | |
| if self.service is not None: | |
| ret += ' service=%s' % self.service | |
| ret += ' at 0x%x>' % id(self) | |
| return ret | |
| pyamf.register_class(RecordSet) | |
| def _check_for_int(x): | |
| """ | |
| This is a compatibility function that takes a C{float} and converts it to an | |
| C{int} if the values are equal. | |
| """ | |
| try: | |
| y = int(x) | |
| except (OverflowError, ValueError): | |
| pass | |
| else: | |
| # There is no way in AMF0 to distinguish between integers and floats | |
| if x == x and y == x: | |
| return y | |
| return x |