package com.gvaneyck.rtmp.encoding; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Decodes AMF3 data and packets from RTMP * * @author Gabriel Van Eyck */ public class AMF3Decoder { /** Stores the data to be consumed while decoding */ private byte[] dataBuffer; private int dataPos; /** Lists of references and class definitions seen so far */ private List<String> stringReferences = new ArrayList<String>(); private List<Object> objectReferences = new ArrayList<Object>(); private List<ClassDefinition> classDefinitions = new ArrayList<ClassDefinition>(); /** * Resets all the reference lists */ public void reset() { stringReferences.clear(); objectReferences.clear(); classDefinitions.clear(); } /** * Decodes the result of a connect call * * @param data The connect result * @return The decoded object * @throws EncodingException * @throws NotImplementedException */ public TypedObject decodeConnect(byte[] data) throws NotImplementedException, EncodingException { reset(); dataBuffer = data; dataPos = 0; TypedObject result = new TypedObject("Invoke"); result.put("result", decodeAMF0()); result.put("invokeId", decodeAMF0()); result.put("serviceCall", decodeAMF0()); result.put("data", decodeAMF0()); if (dataPos != dataBuffer.length) throw new EncodingException("Did not consume entire buffer: " + dataPos + " of " + dataBuffer.length); return result; } /** * Decodes the result of a invoke call * * @param data The invoke result * @return The decoded object * @throws EncodingException * @throws NotImplementedException */ public TypedObject decodeInvoke(byte[] data) throws NotImplementedException, EncodingException { reset(); dataBuffer = data; dataPos = 0; TypedObject result = new TypedObject("Invoke"); if (dataBuffer[0] == 0x00) { dataPos++; result.put("version", 0x00); } result.put("result", decodeAMF0()); result.put("invokeId", decodeAMF0()); result.put("serviceCall", decodeAMF0()); result.put("data", decodeAMF0()); if (dataPos != dataBuffer.length) throw new EncodingException("Did not consume entire buffer: " + dataPos + " of " + dataBuffer.length); return result; } /** * Decodes data according to AMF3 * * @param data The data to decode * @return The decoded object * @throws NotImplementedException * @throws EncodingException */ public Object decode(byte[] data) throws EncodingException, NotImplementedException { dataBuffer = data; dataPos = 0; Object result = decode(); if (dataPos != dataBuffer.length) throw new EncodingException("Did not consume entire buffer: " + dataPos + " of " + dataBuffer.length); return result; } /** * Decodes AMF3 data in the buffer * * @return The decoded object * @throws EncodingException * @throws NotImplementedException */ private Object decode() throws EncodingException, NotImplementedException { byte type = readByte(); switch (type) { case 0x00: throw new EncodingException("Undefined data type"); case 0x01: return null; case 0x02: return false; case 0x03: return true; case 0x04: return readInt(); case 0x05: return readDouble(); case 0x06: return readString(); case 0x07: return readXML(); case 0x08: return readDate(); case 0x09: return readArray(); case 0x0A: return readObject(); case 0x0B: return readXMLString(); case 0x0C: return readByteArray(); } throw new EncodingException("Unexpected AMF3 data type: " + type); } /** * Removes a single byte from the data buffer * * @return The next byte in the data buffer */ private byte readByte() { byte ret = dataBuffer[dataPos]; dataPos++; return ret; } /** * Removes a single byte from the data buffer as an unsigned integer * * @return The next byte in the data buffer as an unsigned integer */ private int readByteAsInt() { int ret = readByte(); if (ret < 0) ret += 256; return ret; } /** * Removes the next 'length' bytes from the data buffer * * @param length The number of bytes to retrieve * @return The next 'length' bytes in the data buffer */ private byte[] readBytes(int length) { byte[] ret = new byte[length]; for (int i = 0; i < length; i++) { ret[i] = dataBuffer[dataPos]; dataPos++; } return ret; } /** * Decodes an AMF3 integer * * @return The decoded integer * @author FluorineFX */ private int readInt() { int ret = readByteAsInt(); int tmp; if (ret < 128) { return ret; } else { ret = (ret & 0x7f) << 7; tmp = readByteAsInt(); if (tmp < 128) { ret = ret | tmp; } else { ret = (ret | tmp & 0x7f) << 7; tmp = readByteAsInt(); if (tmp < 128) { ret = ret | tmp; } else { ret = (ret | tmp & 0x7f) << 8; tmp = readByteAsInt(); ret = ret | tmp; } } } // Sign extend int mask = 1 << 28; int r = -(ret & mask) | ret; return r; } /** * Decodes an AMF3 double * * @return The decoded double */ private double readDouble() { long value = 0; for (int i = 0; i < 8; i++) value = (value << 8) + readByteAsInt(); return Double.longBitsToDouble(value); } /** * Decodes an AMF3 string * * @return The decoded string * @throws EncodingException */ private String readString() throws EncodingException { int handle = readInt(); boolean inline = ((handle & 1) != 0); handle = handle >> 1; if (inline) { if (handle == 0) return ""; byte[] data = readBytes(handle); String str; try { str = new String(data, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new EncodingException("Error parsing AMF3 string from " + data); } stringReferences.add(str); return str; } else { return stringReferences.get(handle); } } /** * Not implemented * * @return * @throws NotImplementedException */ private String readXML() throws NotImplementedException { throw new NotImplementedException("Reading of XML is not implemented"); } /** * Decodes an AMF3 date * * @return The decoded date */ private Date readDate() { int handle = readInt(); boolean inline = ((handle & 1) != 0); handle = handle >> 1; if (inline) { long ms = (long)readDouble(); Date d = new Date(ms); objectReferences.add(d); return d; } else { return (Date)objectReferences.get(handle); } } /** * Decodes an AMF3 (non-associative) array * * @return The decoded array * @throws EncodingException * @throws NotImplementedException */ private Object[] readArray() throws EncodingException, NotImplementedException { int handle = readInt(); boolean inline = ((handle & 1) != 0); handle = handle >> 1; if (inline) { String key = readString(); if (key != null && !key.equals("")) throw new NotImplementedException("Associative arrays are not supported"); Object[] ret = new Object[handle]; objectReferences.add(ret); for (int i = 0; i < handle; i++) ret[i] = decode(); return ret; } else { return (Object[])objectReferences.get(handle); } } /** * Decodes an AMF3 object * * @return The decoded object * @throws EncodingException * @throws NotImplementedException */ private Object readObject() throws EncodingException, NotImplementedException { int handle = readInt(); boolean inline = ((handle & 1) != 0); handle = handle >> 1; if (inline) { boolean inlineDefine = ((handle & 1) != 0); handle = handle >> 1; ClassDefinition cd; if (inlineDefine) { cd = new ClassDefinition(); cd.type = readString(); cd.externalizable = ((handle & 1) != 0); handle = handle >> 1; cd.dynamic = ((handle & 1) != 0); handle = handle >> 1; for (int i = 0; i < handle; i++) cd.members.add(readString()); classDefinitions.add(cd); } else { cd = classDefinitions.get(handle); } TypedObject ret = new TypedObject(cd.type); // Need to add reference here due to circular references objectReferences.add(ret); if (cd.externalizable) { if (cd.type.equals("DSK")) ret = readDSK(); else if (cd.type.equals("DSA")) ret = readDSA(); else if (cd.type.equals("flex.messaging.io.ArrayCollection")) { Object obj = decode(); ret = TypedObject.makeArrayCollection((Object[])obj); } else if (cd.type.equals("com.riotgames.platform.systemstate.ClientSystemStatesNotification") || cd.type.equals("com.riotgames.platform.broadcast.BroadcastNotification")) { int size = 0; for (int i = 0; i < 4; i++) size = size * 256 + readByteAsInt(); String json; try { json = new String(readBytes(size), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new EncodingException(e.toString()); } ret = new TypedObject((ObjectMap)JSON.parse(json)); ret.type = cd.type; } else { for (int i = dataPos; i < dataBuffer.length; i++) System.out.print(String.format("%02X", dataBuffer[i])); System.out.println(); throw new NotImplementedException("Externalizable not handled for " + cd.type); } } else { for (int i = 0; i < cd.members.size(); i++) { String key = cd.members.get(i); Object value = decode(); ret.put(key, value); } if (cd.dynamic) { String key; while ((key = readString()).length() != 0) { Object value = decode(); ret.put(key, value); } } } return ret; } else { return objectReferences.get(handle); } } /** * Not implemented * * @return * @throws NotImplementedException */ private String readXMLString() throws NotImplementedException { throw new NotImplementedException("Reading of XML strings is not implemented"); } /** * Decodes an AMF3 byte array * * @return The decoded byte array */ private byte[] readByteArray() { int handle = readInt(); boolean inline = ((handle & 1) != 0); handle = handle >> 1; if (inline) { byte[] ret = readBytes(handle); objectReferences.add(ret); return ret; } else { return (byte[])objectReferences.get(handle); } } /** * Decodes a DSA * * @return The decoded DSA * @throws NotImplementedException * @throws EncodingException */ private TypedObject readDSA() throws EncodingException, NotImplementedException { TypedObject ret = new TypedObject("DSA"); int flag; List<Integer> flags = readFlags(); for (int i = 0; i < flags.size(); i++) { flag = flags.get(i); int bits = 0; if (i == 0) { if ((flag & 0x01) != 0) ret.put("body", decode()); if ((flag & 0x02) != 0) ret.put("clientId", decode()); if ((flag & 0x04) != 0) ret.put("destination", decode()); if ((flag & 0x08) != 0) ret.put("headers", decode()); if ((flag & 0x10) != 0) ret.put("messageId", decode()); if ((flag & 0x20) != 0) ret.put("timeStamp", decode()); if ((flag & 0x40) != 0) ret.put("timeToLive", decode()); bits = 7; } else if (i == 1) { if ((flag & 0x01) != 0) { readByte(); byte[] temp = readByteArray(); ret.put("clientIdBytes", temp); ret.put("clientId", byteArrayToID(temp)); } if ((flag & 0x02) != 0) { readByte(); byte[] temp = readByteArray(); ret.put("messageIdBytes", temp); ret.put("messageId", byteArrayToID(temp)); } bits = 2; } readRemaining(flag, bits); } flags = readFlags(); for (int i = 0; i < flags.size(); i++) { flag = flags.get(i); int bits = 0; if (i == 0) { if ((flag & 0x01) != 0) ret.put("correlationId", decode()); if ((flag & 0x02) != 0) { readByte(); byte[] temp = readByteArray(); ret.put("correlationIdBytes", temp); ret.put("correlationId", byteArrayToID(temp)); } bits = 2; } readRemaining(flag, bits); } return ret; } /** * Decodes a DSK * * @return The decoded DSK * @throws NotImplementedException * @throws EncodingException */ private TypedObject readDSK() throws EncodingException, NotImplementedException { // DSK is just a DSA + extra set of flags/objects TypedObject ret = readDSA(); ret.type = "DSK"; List<Integer> flags = readFlags(); for (int i = 0; i < flags.size(); i++) readRemaining(flags.get(i), 0); return ret; } private List<Integer> readFlags() { List<Integer> flags = new ArrayList<Integer>(); int flag; do { flag = readByteAsInt(); flags.add(flag); } while ((flag & 0x80) != 0); return flags; } private void readRemaining(int flag, int bits) throws EncodingException, NotImplementedException { // For forwards compatibility, read in any other flagged objects to // preserve the integrity of the input stream... if ((flag >> bits) != 0) { for (int o = bits; o < 6; o++) { if (((flag >> o) & 1) != 0) decode(); } } } /** * Converts an array of bytes into an ID string * * @return The ID string */ private String byteArrayToID(byte[] data) { StringBuilder ret = new StringBuilder(); for (int i = 0; i < data.length; i++) { if (i == 4 || i == 6 || i == 8 || i == 10) ret.append('-'); ret.append(String.format("%02x", data[i])); } return ret.toString(); } /** * Decodes the next AMF0 object from the buffer * * @return The decoded object * @throws NotImplementedException * @throws EncodingException */ private Object decodeAMF0() throws NotImplementedException, EncodingException { int type = readByte(); switch (type) { case 0x00: return readIntAMF0(); case 0x02: return readStringAMF0(); case 0x03: return readObjectAMF0(); case 0x05: return null; case 0x11: // AMF3 return decode(); } throw new NotImplementedException("AMF0 type not supported: " + type); } /** * Decodes an AMF0 string * * @return The decoded string * @throws EncodingException */ private String readStringAMF0() throws EncodingException { int length = (readByteAsInt() << 8) + readByteAsInt(); if (length == 0) return ""; byte[] data = readBytes(length); // UTF-8 applicable? String str; try { str = new String(data, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new EncodingException("Error parsing AMF0 string from " + data); } return str; } /** * Decodes an AMF0 integer * * @return The decoded integer */ private int readIntAMF0() { return (int)readDouble(); } /** * Decodes an AMF0 object * * @return The decoded object * @throws EncodingException * @throws NotImplementedException */ private TypedObject readObjectAMF0() throws EncodingException, NotImplementedException { TypedObject body = new TypedObject("Body"); String key; while (!(key = readStringAMF0()).equals("")) { byte b = readByte(); if (b == 0x00) body.put(key, readDouble()); else if (b == 0x02) body.put(key, readStringAMF0()); else if (b == 0x05) body.put(key, null); else throw new NotImplementedException("AMF0 type not supported: " + b); } readByte(); // Skip object end marker return body; } }