using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using UnityEngine; namespace Mirror { // for performance, we (ab)use c# generics to cache the message id in a static field // this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary // generic classes have separate static fields per type specification public static class NetworkMessageId where T : struct, NetworkMessage { // automated message id from type hash. // platform independent via stable hashcode. // => convenient so we don't need to track messageIds across projects // => addons can work with each other without knowing their ids before // => 2 bytes is enough to avoid collisions. // registering a messageId twice will log a warning anyway. public static readonly ushort Id = CalculateId(); // Gets the 32bit fnv1a hash // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits // This will create a more uniform 16bit hash, the method is described in: // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding" static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16(); } // message packing all in one place, instead of constructing headers in all // kinds of different places // // MsgType (2 bytes) // Content (ContentSize bytes) public static class NetworkMessages { // size of message id header in bytes public const int IdSize = sizeof(ushort); // Id <> Type lookup for debugging, profiler, etc. // important when debugging messageId errors! public static readonly Dictionary Lookup = new Dictionary(); // dump all types for debugging public static void LogTypes() { StringBuilder builder = new StringBuilder(); builder.AppendLine("NetworkMessageIds:"); foreach (KeyValuePair kvp in Lookup) { builder.AppendLine($" Id={kvp.Key} = {kvp.Value}"); } Debug.Log(builder.ToString()); } // max message content size (without header) calculation for convenience // -> Transport.GetMaxPacketSize is the raw maximum // -> Every message gets serialized into <> // -> Every serialized message get put into a batch with one timestamp per batch // -> Every message in a batch has a varuint size header. // use the worst case VarUInt size for the largest possible // message size = int.max. public static int MaxContentSize(int channelId) { // calculate the max possible size that can fit in a batch int transportMax = Transport.active.GetMaxPacketSize(channelId); return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax); } // max message size which includes header + content. public static int MaxMessageSize(int channelId) => MaxContentSize(channelId) + IdSize; // automated message id from type hash. // platform independent via stable hashcode. // => convenient so we don't need to track messageIds across projects // => addons can work with each other without knowing their ids before // => 2 bytes is enough to avoid collisions. // registering a messageId twice will log a warning anyway. // keep this for convenience. easier to use than NetworkMessageId.Id. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort GetId() where T : struct, NetworkMessage => NetworkMessageId.Id; // pack message before sending // -> NetworkWriter passed as arg so that we can use .ToArraySegment // and do an allocation free send before recycling it. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Pack(T message, NetworkWriter writer) where T : struct, NetworkMessage { writer.WriteUShort(NetworkMessageId.Id); writer.Write(message); } // read only the message id. // common function in case we ever change the header size. public static bool UnpackId(NetworkReader reader, out ushort messageId) { // read message type try { messageId = reader.ReadUShort(); return true; } catch (System.IO.EndOfStreamException) { messageId = 0; return false; } } // version for handlers with channelId // inline! only exists for 20-30 messages and they call it all the time. internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication, bool exceptionsDisconnect) where T : struct, NetworkMessage where C : NetworkConnection => (conn, reader, channelId) => { // protect against DOS attacks if attackers try to send invalid // data packets to crash the server/client. there are a thousand // ways to cause an exception in data handling: // - invalid headers // - invalid message ids // - invalid data causing exceptions // - negative ReadBytesAndSize prefixes // - invalid utf8 strings // - etc. // // let's catch them all and then disconnect that connection to avoid // further attacks. T message = default; // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching int startPos = reader.Position; try { if (requireAuthentication && !conn.isAuthenticated) { // message requires authentication, but the connection was not authenticated Debug.LogWarning($"Disconnecting connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet"); conn.Disconnect(); return; } //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}"); // if it is a value type, just use default(T) // otherwise allocate a new instance message = reader.Read(); } catch (Exception exception) { // should we disconnect on exceptions? if (exceptionsDisconnect) { Debug.LogError($"Disconnecting connection: {conn} because reading a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); conn.Disconnect(); return; } // otherwise log it but allow the connection to keep playing else { Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}"); return; } } finally { int endPos = reader.Position; // TODO: Figure out the correct channel NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos); } // user handler exception should not stop the whole server try { // user implemented handler handler((C)conn, message, channelId); } catch (Exception exception) { // should we disconnect on exceptions? if (exceptionsDisconnect) { Debug.LogError($"Disconnecting connection: {conn} because handling a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); conn.Disconnect(); } // otherwise log it but allow the connection to keep playing else { Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}"); } } }; // version for handlers without channelId // TODO obsolete this some day to always use the channelId version. // all handlers in this version are wrapped with 1 extra action. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication, bool exceptionsDisconnect) where T : struct, NetworkMessage where C : NetworkConnection { // wrap action as channelId version, call original void Wrapped(C conn, T msg, int _) => handler(conn, msg); return WrapHandler((Action)Wrapped, requireAuthentication, exceptionsDisconnect); } } }