/* * RED5 Open Source Media Server - https://github.com/Red5/ Copyright 2006-2016 by respective authors (see below). All rights reserved. Licensed under the Apache License, Version * 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless * required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package org.red5.io.amf; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Vector; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.PropertyUtilsBean; import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang3.math.NumberUtils; import org.apache.mina.core.buffer.IoBuffer; import org.red5.io.amf3.ByteArray; import org.red5.io.object.BaseInput; import org.red5.io.object.DataTypes; import org.red5.io.object.Deserializer; import org.red5.io.object.RecordSet; import org.red5.io.object.RecordSetPage; import org.red5.io.utils.ArrayUtils; import org.red5.io.utils.ConversionUtils; import org.red5.io.utils.ObjectMap; import org.red5.io.utils.XMLUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; /** * Input for Red5 data types * * @author The Red5 Project * @author Luke Hubbard, Codegent Ltd ([email protected]) * @author Paul Gregoire (mond[email protected]) */ @SuppressWarnings("serial") public class Input extends BaseInput implements org.red5.io.object.Input { protected Logger log = LoggerFactory.getLogger(this.getClass()); protected static Map<String, String> classAliases = new HashMap<String, String>(3) { { put("DSA", "org.red5.compatibility.flex.messaging.messages.AsyncMessageExt"); put("DSC", "org.red5.compatibility.flex.messaging.messages.CommandMessageExt"); put("DSK", "org.red5.compatibility.flex.messaging.messages.AcknowledgeMessageExt"); } }; protected IoBuffer buf; protected byte currentDataType; /** * Creates Input object from byte buffer * * @param buf * Byte buffer */ public Input(IoBuffer buf) { super(); this.buf = buf; if (log.isTraceEnabled()) { log.trace("Input: {}", Hex.encodeHexString(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()))); } } /** * Reads the data type. * * @return One of AMF class constants with type * @see org.red5.io.amf.AMF */ @Override public byte readDataType() { // prevent the handling of an empty Object if (buf.hasRemaining()) { do { // get the data type currentDataType = buf.get(); log.trace("Data type: {}", currentDataType); switch (currentDataType) { case AMF.TYPE_NULL: case AMF.TYPE_UNDEFINED: return DataTypes.CORE_NULL; case AMF.TYPE_NUMBER: return DataTypes.CORE_NUMBER; case AMF.TYPE_BOOLEAN: return DataTypes.CORE_BOOLEAN; case AMF.TYPE_STRING: case AMF.TYPE_LONG_STRING: return DataTypes.CORE_STRING; case AMF.TYPE_CLASS_OBJECT: case AMF.TYPE_OBJECT: return DataTypes.CORE_OBJECT; case AMF.TYPE_MIXED_ARRAY: return DataTypes.CORE_MAP; case AMF.TYPE_ARRAY: return DataTypes.CORE_ARRAY; case AMF.TYPE_DATE: return DataTypes.CORE_DATE; case AMF.TYPE_XML: return DataTypes.CORE_XML; case AMF.TYPE_REFERENCE: return DataTypes.OPT_REFERENCE; case AMF.TYPE_UNSUPPORTED: case AMF.TYPE_MOVIECLIP: case AMF.TYPE_RECORDSET: // These types are not handled by core datatypes // So add the amf mask to them, this way the deserializer // will call back to readCustom, we can then handle or // return null return (byte) (currentDataType + DataTypes.CUSTOM_AMF_MASK); case AMF.TYPE_AMF3_OBJECT: log.debug("Switch to AMF3"); return DataTypes.CORE_SWITCH; } } while (hasMoreProperties()); log.trace("No more data types available"); return DataTypes.CORE_END_OBJECT; } // empty object, may as well be null return DataTypes.CORE_NULL; } /** * Reads a null. * * @return Object */ @Override public Object readNull() { return null; } /** * Reads a boolean. * * @return boolean */ @Override public Boolean readBoolean() { return (buf.get() == AMF.VALUE_TRUE) ? Boolean.TRUE : Boolean.FALSE; } /** * Reads a Number. In ActionScript 1 and 2 Number type represents all numbers, both floats and integers. * * @return Number */ @Override public Number readNumber() { int remaining = buf.remaining(); log.debug("readNumber from {} bytes", remaining); // look to see if big enough for double if (remaining >= 8) { double d = buf.getDouble(); log.debug("Number: {}", d); return d; } if (log.isDebugEnabled()) { log.debug("Remaining not big enough for number - offset: {} limit: {} {}", buf.position(), buf.limit(), Hex.encodeHexString(buf.array())); } return 0; } /** * Reads string from buffer * * @return String */ @Override public String getString() { log.trace("getString - currentDataType: {}", currentDataType); byte lastDataType = currentDataType; // temporarily set to string for reading if (currentDataType != AMF.TYPE_STRING) { currentDataType = AMF.TYPE_STRING; } String result = readString(); // set data type back to what it was currentDataType = lastDataType; return result; } /** * Reads a string * * @return String */ @Override public String readString() { int limit = buf.limit(); int len = 0; switch (currentDataType) { case AMF.TYPE_LONG_STRING: log.trace("Long string type"); len = buf.getInt(); if (len > limit) { len = limit; } break; case AMF.TYPE_STRING: log.trace("Std string type"); len = buf.getUnsignedShort(); break; default: log.debug("Unknown AMF type: {}", currentDataType); } log.debug("Length: {} limit: {}", len, limit); byte[] str = new byte[len]; buf.get(str); String string = bufferToString(str); return string; } /** * Converts the bytes into a string. * * @param str * string bytes * @return decoded String */ private final String bufferToString(byte[] str) { String string = null; if (str != null) { string = AMF.CHARSET.decode(ByteBuffer.wrap(str)).toString(); log.debug("String: {}", string); } else { log.warn("ByteBuffer was null attempting to read String"); } return string; } /** * Returns a date * * @return Date Decoded string object */ @Override public Date readDate() { /* * Date: 0x0B T7 T6 .. T0 Z1 Z2 T7 to 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. Z1 an Z0 for a 16 bit Big Endian number indicating the indicated time's timezone in minutes. */ long ms = (long) buf.getDouble(); // The timezone can be ignored as the date always is encoded in UTC @SuppressWarnings("unused") short timeZoneMins = buf.getShort(); Date date = new Date(ms); storeReference(date); return date; } @Override public Object readArray(Type target) { log.debug("readArray - target: {}", target); Object result = null; int count = buf.getInt(); log.debug("Count: {}", count); // To conform to the Input API, we should convert the output into an Array if the Type asks us to. Class<?> collection = Collection.class; if (target instanceof Class<?>) { collection = (Class<?>) target; } List<Object> resultCollection = new ArrayList<>(count); if (collection.isArray()) { result = ArrayUtils.getArray(collection.getComponentType(), count); } else { result = resultCollection; } storeReference(result); // reference should be stored before reading of objects to get correct refIds for (int i = 0; i < count; i++) { resultCollection.add(Deserializer.deserialize(this, Object.class)); } if (collection.isArray()) { ArrayUtils.fillArray(collection.getComponentType(), result, resultCollection); } return result; } /** * Read key - value pairs. This is required for the RecordSet deserializer. */ @Override public Map<String, Object> readKeyValues() { Map<String, Object> result = new HashMap<String, Object>(); readKeyValues(result); return result; } /** * Read key - value pairs into Map object * * @param result * Map to put resulting pair to */ protected void readKeyValues(Map<String, Object> result) { while (hasMoreProperties()) { String name = readPropertyName(); log.debug("property: {}", name); Object property = Deserializer.deserialize(this, Object.class); log.debug("val: {}", property); result.put(name, property); if (hasMoreProperties()) { skipPropertySeparator(); } else { break; } } } @Override public Object readMap() { // the maximum number used in this mixed array int maxNumber = buf.getInt(); log.debug("Read start mixed array: {}", maxNumber); ObjectMap<Object, Object> result = new ObjectMap<Object, Object>(); // we must store the reference before we deserialize any items in it to // ensure that reference IDs are correct int reference = storeReference(result); while (hasMoreProperties()) { String key = getString(); Object item = Deserializer.deserialize(this, Object.class); //log.info("key: {} item: {}", key, item); if (!NumberUtils.isParsable(key)) { result.put(key, item); } else { // map keys are either integers or strings, none will be doubles if (key.contains(".")) { result.put(key, item); } else { result.put(Integer.valueOf(key), item); } } } result.remove("length"); // replace the original reference with the final result storeReference(reference, result); return result; } /** * Creates a new instance of the className parameter and returns as an Object * * @param className * Class name as String * @return Object New object instance (for given class) */ @SuppressWarnings("all") protected Object newInstance(String className) { log.debug("Loading class: {}", className); Object instance = null; Class<?> clazz = null; if ("".equals(className) || className == null) return instance; try { // check for special DS class aliases if (className.length() == 3) { className = classAliases.get(className); } if (className.startsWith("flex.")) { // Use Red5 compatibility class instead className = "org.red5.compatibility." + className; log.debug("Modified classname: {}", className); } if (!classAllowed(className)) { log.error("Class creation is not allowed {}", className); } else { clazz = Thread.currentThread().getContextClassLoader().loadClass(className); instance = clazz.newInstance(); } } catch (InstantiationException iex) { try { // check for default ctor clazz.getConstructor(null); log.error("Error loading class: {}", className); } catch (NoSuchMethodException nse) { log.error("Error loading class: {}; this can be resolved by adding a default constructor to your class", className); } log.debug("Exception was: {}", iex); } catch (Exception ex) { log.error("Error loading class: {}", className); log.debug("Exception was: {}", ex); } return instance; } /** * Reads the input as a bean and returns an object * * @param bean * Input as bean * @return Decoded object */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected Object readBean(Object bean) { log.debug("readBean: {}", bean); storeReference(bean); Class theClass = bean.getClass(); while (hasMoreProperties()) { String name = readPropertyName(); Type type = getPropertyType(bean, name); log.debug("property: {} type: {}", name, type); Object property = Deserializer.deserialize(this, type); log.debug("val: {}", property); // log.debug("val: "+property.getClass().getName()); if (property != null) { try { if (type instanceof Class) { Class t = (Class) type; if (!t.isAssignableFrom(property.getClass())) { property = ConversionUtils.convert(property, t); } } final Field field = theClass.getField(name); field.set(bean, property); } catch (Exception ex2) { try { BeanUtils.setProperty(bean, name, property); } catch (Exception ex) { log.error("Error mapping property: {} ({})", name, property); } } } else { log.debug("Skipping null property: {}", name); } if (hasMoreProperties()) { skipPropertySeparator(); } else { break; // hasMoreProperties == false, position moved to +3 } } return bean; } /** * Reads the input as a map and returns a Map * * @return Read map */ protected Map<String, Object> readSimpleObject() { log.debug("readSimpleObject"); Map<String, Object> result = new ObjectMap<>(); readKeyValues(result); storeReference(result); return result; } /** * Reads start object * * @return Read object */ @Override public Object readObject() { String className; if (currentDataType == AMF.TYPE_CLASS_OBJECT) { className = getString(); log.debug("readObject: {}", className); if (className != null) { log.debug("read class object"); Object result = null; Object instance; if (className.equals("RecordSet")) { result = new RecordSet(this); storeReference(result); } else if (className.equals("RecordSetPage")) { result = new RecordSetPage(this); storeReference(result); } else if (!classAllowed(className)) { log.debug("Class creation is not allowed {}", className); result = readSimpleObject(); } else { instance = newInstance(className); if (instance != null) { result = readBean(instance); } else { log.debug("Forced to use simple object for class {}", className); result = readSimpleObject(); } } return result; } } return readSimpleObject(); } /** * Returns a boolean stating whether there are more properties * * @return boolean <code>true</code> if there are more properties to read, <code>false</code> otherwise */ public boolean hasMoreProperties() { if (buf.remaining() >= 3) { byte[] threeBytes = new byte[3]; int pos = buf.position(); buf.get(threeBytes); if (Arrays.equals(AMF.END_OF_OBJECT_SEQUENCE, threeBytes)) { log.trace("End of object"); return false; } buf.position(pos); return true; } // an end-of-object marker can't occupy less than 3 bytes so return true return true; } /** * Reads property name * * @return String Object property name */ public String readPropertyName() { return getString(); } /** * Skips property separator */ public void skipPropertySeparator() { // SKIP } /** * Reads XML * * @return String XML as string */ @Override public Document readXML() { final String xmlString = readString(); Document doc = null; try { doc = XMLUtils.stringToDoc(xmlString); } catch (IOException ioex) { log.error("IOException converting xml to dom", ioex); } storeReference(doc); return doc; } /** * Reads Custom * * @return Object Custom type object */ @Override public Object readCustom() { // return null for now return null; } /** * Read ByteArray object. This is not supported by the AMF0 deserializer. * * @return ByteArray object */ @Override public ByteArray readByteArray() { throw new RuntimeException("ByteArray objects not supported with AMF0"); } /** * Read Vector<int> object. This is not supported by the AMF0 deserializer. * * @return Vector<Integer> object */ @Override public Vector<Integer> readVectorInt() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Read Vector<Long> object. This is not supported by the AMF0 deserializer. * * @return Vector<Long> object */ @Override public Vector<Long> readVectorUInt() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Read Vector<Number> object. This is not supported by the AMF0 deserializer. * * @return Vector<Double> object */ @Override public Vector<Double> readVectorNumber() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Read Vector<Object> object. This is not supported by the AMF0 deserializer. * * @return Vector<Object> object */ @Override public Vector<Object> readVectorObject() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Reads Reference * * @return Object Read reference to object */ @Override public Object readReference() { return getReference(buf.getUnsignedShort()); } /** {@inheritDoc} */ @Override public void reset() { clearReferences(); } protected Type getPropertyType(Object instance, String propertyName) { try { if (instance != null) { Field field = instance.getClass().getField(propertyName); return field.getGenericType(); } else { // instance is null for anonymous class, use default type } } catch (NoSuchFieldException e1) { try { BeanUtilsBean beanUtilsBean = BeanUtilsBean.getInstance(); PropertyUtilsBean propertyUtils = beanUtilsBean.getPropertyUtils(); PropertyDescriptor propertyDescriptor = propertyUtils.getPropertyDescriptor(instance, propertyName); return propertyDescriptor.getReadMethod().getGenericReturnType(); } catch (Exception e2) { // nothing } } catch (Exception e) { // ignore other exceptions } // return Object class type by default return Object.class; } }