using FishNet.Authenticating; using FishNet.Component.Observing; using FishNet.Connection; using FishNet.Managing.Debugging; using FishNet.Managing.Logging; using FishNet.Managing.Predicting; using FishNet.Managing.Transporting; using FishNet.Object; using FishNet.Serializing; using FishNet.Transporting; using FishNet.Utility.Extension; using FishNet.Utility.Performance; using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace FishNet.Managing.Server { /// /// A container for server data and actions. /// [DisallowMultipleComponent] [AddComponentMenu("FishNet/Manager/ServerManager")] public sealed partial class ServerManager : MonoBehaviour { #region Public. /// /// Called after the local server connection state changes. /// public event Action OnServerConnectionState; /// /// Called when authenticator has concluded a result for a connection. Boolean is true if authentication passed, false if failed. /// public event Action OnAuthenticationResult; /// /// Called when a remote client state changes with the server. /// public event Action OnRemoteConnectionState; /// /// True if the server connection has started. /// public bool Started { get; private set; } /// /// Handling and information for objects on the server. /// public ServerObjects Objects { get; private set; } /// /// Authenticated and non-authenticated connected clients. /// [HideInInspector] public Dictionary Clients = new Dictionary(); /// /// NetworkManager for server. /// [HideInInspector] public NetworkManager NetworkManager { get; private set; } #endregion #region Serialized. /// /// Authenticator for this ServerManager. May be null if not using authentication. /// [Obsolete("Use GetAuthenticator and SetAuthenticator.")] //Remove on 2023/06/01 public Authenticator Authenticator { get => GetAuthenticator(); set => SetAuthenticator(value); } /// /// Gets the Authenticator for this manager. /// /// public Authenticator GetAuthenticator() => _authenticator; /// /// Gets the Authenticator for this manager, and initializes it. /// /// public void SetAuthenticator(Authenticator value) { _authenticator = value; InitializeAuthenticator(); } [Tooltip("Authenticator for this ServerManager. May be null if not using authentication.")] [SerializeField] private Authenticator _authenticator; /// /// Default send rate for SyncTypes. A value of 0f will send changed values every tick. /// SyncTypeRate cannot yet be changed at runtime because this would require recalculating rates on SyncBase, which is not yet implemented. /// /// internal float GetSynctypeRate() => _syncTypeRate; [Tooltip("Default send rate for SyncTypes. A value of 0f will send changed values every tick.")] [Range(0f, 60f)] [SerializeField] private float _syncTypeRate = 0.1f; /// /// How to pack object spawns. /// [Tooltip("How to pack object spawns.")] [SerializeField] internal TransformPackingData SpawnPacking = new TransformPackingData() { Position = AutoPackType.Unpacked, Rotation = AutoPackType.PackedLess, Scale = AutoPackType.PackedLess }; /// /// True to automatically set the frame rate when the client connects. /// [Tooltip("True to automatically set the frame rate when the client connects.")] [SerializeField] private bool _changeFrameRate = true; /// /// Maximum frame rate the server may run at. When as host this value runs at whichever is higher between client and server. /// internal ushort FrameRate => (_changeFrameRate) ? _frameRate : (ushort)0; [Tooltip("Maximum frame rate the server may run at. When as host this value runs at whichever is higher between client and server.")] [Range(1, NetworkManager.MAXIMUM_FRAMERATE)] [SerializeField] private ushort _frameRate = NetworkManager.MAXIMUM_FRAMERATE; /// /// True to share the Ids of clients and the objects they own with other clients. No sensitive information is shared. /// internal bool ShareIds => _shareIds; [Tooltip("True to share the Ids of clients and the objects they own with other clients. No sensitive information is shared.")] [SerializeField] private bool _shareIds = true; /// /// Gets StartOnHeadless value. /// public bool GetStartOnHeadless() => _startOnHeadless; /// /// Sets StartOnHeadless value. /// /// New value to use. public void SetStartOnHeadless(bool value) => _startOnHeadless = value; [Tooltip("True to automatically start the server connection when running as headless.")] [SerializeField] private bool _startOnHeadless = true; /// /// True to kick clients which send data larger than the MTU. /// internal bool LimitClientMTU => _limitClientMTU; [Tooltip("True to kick clients which send data larger than the MTU.")] [SerializeField] private bool _limitClientMTU = true; #endregion #region Private. /// /// Used to read splits. /// private SplitReader _splitReader = new SplitReader(); #if UNITY_EDITOR || DEVELOPMENT_BUILD /// /// Logs data about parser to help debug. /// private ParseLogger _parseLogger = new ParseLogger(); #endif #endregion private void OnDestroy() { Objects?.SubscribeToSceneLoaded(false); } /// /// Initializes this script for use. /// /// internal void InitializeOnce_Internal(NetworkManager manager) { NetworkManager = manager; Objects = new ServerObjects(manager); Objects.SubscribeToSceneLoaded(true); InitializeRpcLinks(); //Unsubscribe first incase already subscribed. SubscribeToTransport(false); SubscribeToTransport(true); NetworkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState; NetworkManager.SceneManager.OnClientLoadedStartScenes += SceneManager_OnClientLoadedStartScenes; if (_authenticator == null) _authenticator = GetComponent(); if (_authenticator != null) InitializeAuthenticator(); _cachedLevelOfDetailInterval = NetworkManager.ClientManager.LevelOfDetailInterval; _cachedUseLod = NetworkManager.ObserverManager.GetUseNetworkLod(); } /// /// Initializes the authenticator to this manager. /// private void InitializeAuthenticator() { Authenticator auth = GetAuthenticator(); if (auth == null || auth.Initialized) return; if (NetworkManager == null) return; auth.InitializeOnce(NetworkManager); auth.OnAuthenticationResult += _authenticator_OnAuthenticationResult; } /// /// Starts the server if configured to for headless. /// internal void StartForHeadless() { if (GetStartOnHeadless()) { //Wrapping logic in check instead of everything so _startOnHeadless doesnt warn as unused in editor. #if UNITY_SERVER StartConnection(); #endif } } /// /// Stops the local server connection. /// /// True to send a disconnect message to all clients first. public bool StopConnection(bool sendDisconnectMessage) { if (sendDisconnectMessage) SendDisconnectMessages(Clients.Values.ToList(), true); //Return stop connection result. return NetworkManager.TransportManager.Transport.StopConnection(true); } /// /// Sends a disconnect messge to connectionIds. /// This does not iterate outgoing automatically. /// /// internal void SendDisconnectMessages(int[] connectionIds) { List conns = new List(); foreach (int item in connectionIds) { if (Clients.TryGetValueIL2CPP(item, out NetworkConnection c)) conns.Add(c); } if (conns.Count > 0) SendDisconnectMessages(conns, false); } /// /// Sends a disconnect message to all clients and immediately iterates outgoing. /// private void SendDisconnectMessages(List conns, bool iterate) { PooledWriter writer = WriterPool.GetWriter(); writer.WritePacketId(PacketId.Disconnect); ArraySegment segment = writer.GetArraySegment(); //Send segment to each client, authenticated or not. foreach (NetworkConnection c in conns) c.SendToClient((byte)Channel.Reliable, segment); //Recycle writer. writer.Dispose(); if (iterate) NetworkManager.TransportManager.IterateOutgoing(true); } /// /// Starts the local server connection. /// public bool StartConnection() { return NetworkManager.TransportManager.Transport.StartConnection(true); } /// /// Starts the local server using port. /// /// Port to start on. /// public bool StartConnection(ushort port) { Transport t = NetworkManager.TransportManager.Transport; t.SetPort(port); return t.StartConnection(true); } /// /// Called after the local client connection state changes. /// private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj) { /* If client is doing anything but started destroy pending. * Pending is only used for host mode. */ if (obj.ConnectionState != LocalConnectionState.Started) Objects.DestroyPending(); } /// /// Called when a client loads initial scenes after connecting. /// private void SceneManager_OnClientLoadedStartScenes(NetworkConnection conn, bool asServer) { if (asServer) { Objects.RebuildObservers(conn); /* If connection is host then renderers must be hidden * for all objects not visible to the host. The observer system * does handle this but only after an initial state is set. * If the clientHost joins without observation of an object * then the initial state will never be set. */ if (conn.IsLocalClient) { foreach (NetworkObject nob in Objects.Spawned.Values) { if (!nob.Observers.Contains(conn)) nob.SetRenderersVisible(false); } } } } /// /// Changes subscription status to transport. /// /// private void SubscribeToTransport(bool subscribe) { if (NetworkManager == null || NetworkManager.TransportManager == null || NetworkManager.TransportManager.Transport == null) return; if (subscribe) { NetworkManager.TransportManager.Transport.OnServerReceivedData += Transport_OnServerReceivedData; NetworkManager.TransportManager.Transport.OnServerConnectionState += Transport_OnServerConnectionState; NetworkManager.TransportManager.Transport.OnRemoteConnectionState += Transport_OnRemoteConnectionState; } else { NetworkManager.TransportManager.Transport.OnServerReceivedData -= Transport_OnServerReceivedData; NetworkManager.TransportManager.Transport.OnServerConnectionState -= Transport_OnServerConnectionState; NetworkManager.TransportManager.Transport.OnRemoteConnectionState -= Transport_OnRemoteConnectionState; } } /// /// Called when authenticator has concluded a result for a connection. Boolean is true if authentication passed, false if failed. /// Server listens for this event automatically. /// private void _authenticator_OnAuthenticationResult(NetworkConnection conn, bool authenticated) { if (!authenticated) conn.Disconnect(false); else ClientAuthenticated(conn); } /// /// Called when a connection state changes for the local server. /// private void Transport_OnServerConnectionState(ServerConnectionStateArgs args) { /* Let the client manager know the server state is changing first. * This gives the client an opportunity to clean-up or prepare * before the server completes it's actions. */ Started = AnyServerStarted(); NetworkManager.ClientManager.Objects.OnServerConnectionState(args); //If no servers are started then reset match conditions. if (!Started) { MatchCondition.ClearMatchesWithoutRebuilding(); //Despawn without synchronizing network objects. Objects.DespawnWithoutSynchronization(true); } Objects.OnServerConnectionState(args); LocalConnectionState state = args.ConnectionState; if (NetworkManager.CanLog(LoggingType.Common)) { Transport t = NetworkManager.TransportManager.GetTransport(args.TransportIndex); string tName = (t == null) ? "Unknown" : t.GetType().Name; Debug.Log($"Local server is {state.ToString().ToLower()} for {tName}."); } NetworkManager.UpdateFramerate(); OnServerConnectionState?.Invoke(args); } /// /// Called when a connection state changes for a remote client. /// private void Transport_OnRemoteConnectionState(RemoteConnectionStateArgs args) { //Sanity check to make sure transports are following proper types/ranges. int id = args.ConnectionId; int maxIdValue = short.MaxValue; if (id < 0 || id > maxIdValue) { Kick(args.ConnectionId, KickReason.UnexpectedProblem, LoggingType.Error, $"The transport you are using supplied an invalid connection Id of {id}. Connection Id values must range between 0 and {maxIdValue}. The client has been disconnected."); return; } //Valid Id. else { //If started then add to authenticated clients. if (args.ConnectionState == RemoteConnectionState.Started) { NetworkManager.Log($"Remote connection started for Id {id}."); NetworkConnection conn = new NetworkConnection(NetworkManager, id, true); Clients.Add(args.ConnectionId, conn); OnRemoteConnectionState?.Invoke(conn, args); //Connection is no longer valid. This can occur if the user changes the state using the OnRemoteConnectionState event. if (!conn.IsValid) return; /* If there is an authenticator * and the transport is not a local transport. */ Authenticator auth = GetAuthenticator(); if (auth != null && !NetworkManager.TransportManager.IsLocalTransport(id)) auth.OnRemoteConnection(conn); else ClientAuthenticated(conn); } //If stopping. else if (args.ConnectionState == RemoteConnectionState.Stopped) { /* If client's connection is found then clean * them up from server. */ if (Clients.TryGetValueIL2CPP(id, out NetworkConnection conn)) { conn.SetDisconnecting(true); OnRemoteConnectionState?.Invoke(conn, args); Clients.Remove(id); MatchCondition.RemoveFromMatchWithoutRebuild(conn, NetworkManager); Objects.ClientDisconnected(conn); BroadcastClientConnectionChange(false, conn); //Return predictedObjectIds. Queue pqId = conn.PredictedObjectIds; while (pqId.Count > 0) Objects.CacheObjectId(pqId.Dequeue()); conn.Reset(); NetworkManager.Log($"Remote connection stopped for Id {id}."); } } } } /// /// Sends client their connectionId. /// /// private void SendAuthenticated(NetworkConnection conn) { using (PooledWriter writer = WriterPool.GetWriter()) { writer.WritePacketId(PacketId.Authenticated); writer.WriteNetworkConnection(conn); /* If predicted spawning is enabled then also send * reserved objectIds. */ ; PredictionManager pm = NetworkManager.PredictionManager; if (pm.GetAllowPredictedSpawning()) { int count = Mathf.Min(Objects.GetObjectIdCache().Count, pm.GetReservedObjectIds()); writer.WriteByte((byte)count); for (int i = 0; i < count; i++) { ushort val = (ushort)Objects.GetNextNetworkObjectId(false); writer.WriteNetworkObjectId(val); conn.PredictedObjectIds.Enqueue(val); } } NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, writer.GetArraySegment(), conn); } } /// /// Called when the server socket receives data. /// private void Transport_OnServerReceivedData(ServerReceivedDataArgs args) { args.Data = NetworkManager.TransportManager.ProcessIntermediateIncoming(args.Data, false); ParseReceived(args); } /// /// Called when the server receives data. /// /// private void ParseReceived(ServerReceivedDataArgs args) { #if UNITY_EDITOR || DEVELOPMENT_BUILD _parseLogger.Reset(); #endif //Not from a valid connection. if (args.ConnectionId < 0) return; ArraySegment segment = args.Data; NetworkManager.StatisticsManager.NetworkTraffic.LocalServerReceivedData((ulong)segment.Count); if (segment.Count <= TransportManager.TICK_BYTES) return; //FishNet internally splits packets so nothing should ever arrive over MTU. int channelMtu = NetworkManager.TransportManager.GetMTU(args.TransportIndex, (byte)args.Channel); //If over MTU kick client immediately. if (segment.Count > channelMtu && !NetworkManager.TransportManager.IsLocalTransport(args.ConnectionId)) { ExceededMTUKick(); return; } PacketId packetId = PacketId.Unset; #if !UNITY_EDITOR && !DEVELOPMENT_BUILD try { #endif using (PooledReader reader = ReaderPool.GetReader(segment, NetworkManager)) { uint tick = reader.ReadUInt32(AutoPackType.Unpacked); NetworkManager.TimeManager.LastPacketTick = tick; /* This is a special condition where a message may arrive split. * When this occurs buffer each packet until all packets are * received. */ if (reader.PeekPacketId() == PacketId.Split) { //Skip packetId. reader.ReadPacketId(); int expectedMessages; _splitReader.GetHeader(reader, out expectedMessages); //If here split message can be written. _splitReader.Write(NetworkManager.TimeManager.LastPacketTick, reader, expectedMessages); /* If fullMessage returns 0 count then the split * has not written fully yet. Otherwise, if there is * data within then reinitialize reader with the * full message. */ ArraySegment fullMessage = _splitReader.GetFullMessage(); if (fullMessage.Count == 0) return; /* If here then all data has been received. * It's possible the client could have exceeded * maximum MTU but not the maximum number of splits. * This is because the length of each split * is not written, so we don't know how much data of the * final message actually belonged to the split vs * unrelated data added afterwards. We're going to cut * the client some slack in this situation for the sake * of keeping things simple. */ //Initialize reader with full message. reader.Initialize(fullMessage, NetworkManager); } //Parse reader. while (reader.Remaining > 0) { packetId = reader.ReadPacketId(); #if UNITY_EDITOR || DEVELOPMENT_BUILD _parseLogger.AddPacket(packetId); #endif NetworkConnection conn; /* Connection isn't available. This should never happen. * Force an immediate disconnect. */ if (!Clients.TryGetValueIL2CPP(args.ConnectionId, out conn)) { Kick(args.ConnectionId, KickReason.UnexpectedProblem, LoggingType.Error, $"ConnectionId {conn.ClientId} not found within Clients. Connection will be kicked immediately."); return; } conn.SetLastPacketTick(tick); /* If connection isn't authenticated and isn't a broadcast * then disconnect client. If a broadcast then process * normally; client may still become disconnected if the broadcast * does not allow to be called while not authenticated. */ if (!conn.Authenticated && packetId != PacketId.Broadcast) { conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} sent a Broadcast without being authenticated. Connection will be kicked immediately."); return; } //Only check if not developer build because users pay pause editor. #if !DEVELOPMENT_BUILD && !UNITY_EDITOR /* If hasn't sent LOD recently enough. LODs are sent every half a second, so * by multiplaying interval by 60 this gives the client a 30 second window. */ if (_cachedUseLod && conn.IsLateForLevelOfDetail(_cachedLevelOfDetailInterval * 60)) { conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} has gone too long without sending a level of detail update. Connection will be kicked immediately."); return; } #endif if (packetId == PacketId.Replicate) { Objects.ParseReplicateRpc(reader, conn, args.Channel); } else if (packetId == PacketId.ServerRpc) { Objects.ParseServerRpc(reader, conn, args.Channel); } else if (packetId == PacketId.ObjectSpawn) { if (!NetworkManager.PredictionManager.GetAllowPredictedSpawning()) { conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} sent a predicted spawn while predicted spawning is not enabled. Connection will be kicked immediately."); return; } Objects.ReadPredictedSpawn(reader, conn); } else if (packetId == PacketId.ObjectDespawn) { if (!NetworkManager.PredictionManager.GetAllowPredictedSpawning()) { conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} sent a predicted spawn while predicted spawning is not enabled. Connection will be kicked immediately."); return; } Objects.ReadPredictedDespawn(reader, conn); } else if (packetId == PacketId.NetworkLODUpdate) { ParseNetworkLODUpdate(reader, conn); } else if (packetId == PacketId.Broadcast) { ParseBroadcast(reader, conn, args.Channel); } else if (packetId == PacketId.PingPong) { ParsePingPong(reader, conn); } else { #if UNITY_EDITOR || DEVELOPMENT_BUILD NetworkManager.LogError($"Server received an unhandled PacketId of {(ushort)packetId} from connectionId {args.ConnectionId}. Remaining data has been purged."); _parseLogger.Print(NetworkManager); #else NetworkManager.LogError($"Server received an unhandled PacketId of {(ushort)packetId} from connectionId {args.ConnectionId}. Connection will be kicked immediately."); NetworkManager.TransportManager.Transport.StopConnection(args.ConnectionId, true); #endif return; } } } #if !UNITY_EDITOR && !DEVELOPMENT_BUILD } catch (Exception e) { Kick(args.ConnectionId, KickReason.MalformedData, LoggingType.Error, $"Server encountered an error while parsing data for packetId {packetId} from connectionId {args.ConnectionId}. Connection will be kicked immediately. Message: {e.Message}."); } #endif //Kicks connection for exceeding MTU. void ExceededMTUKick() { Kick(args.ConnectionId, KickReason.ExploitExcessiveData, LoggingType.Common, $"ConnectionId {args.ConnectionId} sent a message larger than allowed amount. Connection will be kicked immediately."); } } /// /// Parses a received PingPong. /// /// /// private void ParsePingPong(PooledReader reader, NetworkConnection conn) { /* //security limit how often clients can send pings. * have clients use a stopwatch rather than frame time * for checks to ensure it's not possible to send * excessively should their game stutter then catch back up. */ uint clientTick = reader.ReadUInt32(AutoPackType.Unpacked); if (conn.CanPingPong()) NetworkManager.TimeManager.SendPong(conn, clientTick); } /// /// Called when a remote client authenticates with the server. /// /// private void ClientAuthenticated(NetworkConnection connection) { /* Immediately send connectionId to client. Some transports * don't give clients their remoteId, therefor it has to be sent * by the ServerManager. This packet is very simple and can be built * on the spot. */ connection.ConnectionAuthenticated(); /* Send client Ids before telling the client * they are authenticated. This is important because when the client becomes * authenticated they set their LocalConnection using Clients field in ClientManager, * which is set after getting Ids. */ BroadcastClientConnectionChange(true, connection); SendAuthenticated(connection); OnAuthenticationResult?.Invoke(connection, true); NetworkManager.SceneManager.OnClientAuthenticated(connection); } /// /// Sends a client connection state change to owner and other clients if applicable. /// private void BroadcastClientConnectionChange(bool connected, NetworkConnection conn) { //If sharing Ids then send all connected client Ids first if is a connected state. if (ShareIds) { /* Send a broadcast to all authenticated clients with the clientId * that just connected. The conn client will also get this. */ ClientConnectionChangeBroadcast changeMsg = new ClientConnectionChangeBroadcast() { Connected = connected, Id = conn.ClientId }; Broadcast(changeMsg); /* If state is connected then the conn client * must also receive all currently connected client ids. */ if (connected) { //Send already connected clients to the connection that just joined. ListCache lc = ListCaches.GetIntCache(); foreach (int key in Clients.Keys) lc.AddValue(key); ConnectedClientsBroadcast allMsg = new ConnectedClientsBroadcast() { ListCache = lc }; conn.Broadcast(allMsg); ListCaches.StoreCache(lc); } } //If not sharing Ids then only send ConnectionChange to conn. else { if (connected) { /* Send broadcast only to the client which just disconnected. * Only send if connecting. If the client is disconnected there's no reason * to send them a disconnect msg. */ ClientConnectionChangeBroadcast changeMsg = new ClientConnectionChangeBroadcast() { Connected = connected, Id = conn.ClientId }; Broadcast(conn, changeMsg, true, Channel.Reliable); } } } } }