# 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) } @see: U{Official AMF0 Specification in Japanese (external) } @see: U{AMF documentation on OSFlash (external) } """ 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 #: } 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} 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} """ 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} """ 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} 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} 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) } """ 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