context: #region Copyright & License Information
  /*
   * Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
   * This file is part of OpenRA, which is free software. It is made
   * available to you under the terms of the GNU General Public License
   * as published by the Free Software Foundation, either version 3 of
   * the License, or (at your option) any later version. For more
   * information, see COPYING.
   */
  #endregion
  
  using System;
  using System.Collections.Concurrent;
  using System.Collections.Generic;
  using System.Globalization;
  using System.IO;
  using System.Linq;
  using System.Net;
  using System.Net.Sockets;
  using System.Text;
  using System.Threading;
  using System.Threading.Tasks;
  using OpenRA.FileFormats;
  using OpenRA.Network;
  using OpenRA.Primitives;
  using OpenRA.Support;
  using OpenRA.Traits;
  
  namespace OpenRA.Server
  {
  	public enum ServerState
  	{
  		WaitingPlayers = 1,
  		GameStarted = 2,
  		ShuttingDown = 3
  	}
  
  	public enum ServerType
  	{
  		Local = 0,
  		Multiplayer = 1,
  		Dedicated = 2
  	}
  
  	public sealed class Server
  	{
  		public readonly string TwoHumansRequiredText = "This server requires at least two human players to start a match.";
  
  		public readonly MersenneTwister Random = new MersenneTwister();
  		public readonly ServerType Type;
  
  		public List<Connection> Conns = new List<Connection>();
  
  		public Session LobbyInfo;
  		public ServerSettings Settings;
  		public ModData ModData;
  		public List<string> TempBans = new List<string>();
  
  		// Managed by LobbyCommands
  		public MapPreview Map;
  		public readonly MapStatusCache MapStatusCache;
  		public GameSave GameSave = null;
  
  		// Default to the next frame for ServerType.Local - MP servers take the value from the selected GameSpeed.
  		public int OrderLatency = 1;
  
  		readonly int randomSeed;
  		readonly List<TcpListener> listeners = new List<TcpListener>();
  		readonly TypeDictionary serverTraits = new TypeDictionary();
  		readonly PlayerDatabase playerDatabase;
  
  		volatile ServerState internalState = ServerState.WaitingPlayers;
  
  		readonly BlockingCollection<IServerEvent> events = new BlockingCollection<IServerEvent>();
  
  		ReplayRecorder recorder;
  		GameInformation gameInfo;
  		readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
  
  		public ServerState State
  		{
  			get => internalState;
  			set => internalState = value;
  		}
  
  		public static void SyncClientToPlayerReference(Session.Client c, PlayerReference pr)
  		{
  			if (pr == null)
  				return;
  
  			if (pr.LockFaction)
  				c.Faction = pr.Faction;
  			if (pr.LockSpawn)
  				c.SpawnPoint = pr.Spawn;
  			if (pr.LockTeam)
  				c.Team = pr.Team;
  			if (pr.LockHandicap)
  				c.Handicap = pr.Handicap;
  
  			c.Color = pr.LockColor ? pr.Color : c.PreferredColor;
  		}
  
  		public void Shutdown()
  		{
  			State = ServerState.ShuttingDown;
  		}
  
  		public void EndGame()
  		{
  			foreach (var t in serverTraits.WithInterface<IEndGame>())
  				t.GameEnded(this);
  
  			recorder?.Dispose();
  			recorder = null;
  		}
  
  		// Craft a fake handshake request/response because that's the
  		// only way to expose the Version and OrdersProtocol.
  		public void RecordFakeHandshake()
  		{
  			var request = new HandshakeRequest
  			{
  				Mod = ModData.Manifest.Id,
  				Version = ModData.Manifest.Metadata.Version,
  			};
  
  			recorder.ReceiveFrame(0, 0, new Order("HandshakeRequest", null, false)
  			{
  				Type = OrderType.Handshake,
  				IsImmediate = true,
  				TargetString = request.Serialize(),
  			}.Serialize());
  
  			var response = new HandshakeResponse()
  			{
  				Mod = ModData.Manifest.Id,
  				Version = ModData.Manifest.Metadata.Version,
  				OrdersProtocol = ProtocolVersion.Orders,
  				Client = new Session.Client(),
  			};
  
  			recorder.ReceiveFrame(0, 0, new Order("HandshakeResponse", null, false)
  			{
  				Type = OrderType.Handshake,
  				IsImmediate = true,
  				TargetString = response.Serialize(),
  			}.Serialize());
  		}
  
  		void MapStatusChanged(string uid, Session.MapStatus status)
  		{
  			lock (LobbyInfo)
  			{
  				if (LobbyInfo.GlobalSettings.Map == uid)
  					LobbyInfo.GlobalSettings.MapStatus = status;
  
  				SyncLobbyInfo();
  			}
  		}
  
  		public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modData, ServerType type)
  		{
  			Log.AddChannel("server", "server.log", true);
  
  			SocketException lastException = null;
  			foreach (var endpoint in endpoints)
  			{
  				var listener = new TcpListener(endpoint);
  				try
  				{
  					try
  					{
  						listener.Server.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 1);
  					}
  					catch (Exception ex)
  					{
  						if (ex is SocketException || ex is ArgumentException)
  							Log.Write("server", "Failed to set socket option on {0}: {1}", endpoint.ToString(), ex.Message);
  						else
  							throw;
  					}
  
  					listener.Start();
  					listeners.Add(listener);
  
  					new Thread(() =>
  					{
  						while (true)
  						{
  							if (State != ServerState.WaitingPlayers)
  							{
  								listener.Stop();
  								return;
  							}
  
  							// Use a 1s timeout so we can stop listening once the game starts
  							if (listener.Server.Poll(1000000, SelectMode.SelectRead))
  							{
  								try
  								{
  									events.Add(new ClientConnectEvent(listener.AcceptSocket()));
  								}
  								catch (Exception)
  								{
  									// Ignore the exception that may be generated if the connection
  									// drops while we are trying to connect
  								}
  							}
  						}
  					}) { Name = $"Connection listener ({listener.LocalEndpoint})", IsBackground = true }.Start();
  				}
  				catch (SocketException ex)
  				{
  					lastException = ex;
  					Log.Write("server", "Failed to listen on {0}: {1}", endpoint.ToString(), ex.Message);
  				}
  			}
  
  			if (listeners.Count == 0)
  				throw lastException;
  
  			Type = type;
  			Settings = settings;
  
  			Settings.Name = Game.Settings.SanitizedServerName(Settings.Name);
  
  			ModData = modData;
  
  			playerDatabase = modData.Manifest.Get<PlayerDatabase>();
  
  			randomSeed = (int)DateTime.Now.ToBinary();
  
  			if (type != ServerType.Local && settings.EnableGeoIP)
  				GeoIP.Initialize();
  
  			if (type != ServerType.Local)
  				Nat.TryForwardPort(Settings.ListenPort, Settings.ListenPort);
  
  			foreach (var trait in modData.Manifest.ServerTraits)
  				serverTraits.Add(modData.ObjectCreator.CreateObject<ServerTrait>(trait));
  
  			serverTraits.TrimExcess();
  
  			Map = ModData.MapCache[settings.Map];
  			MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
  
  			LobbyInfo = new Session
  			{
  				GlobalSettings =
  				{
  					RandomSeed = randomSeed,
  					Map = Map.Uid,
  					MapStatus = Session.MapStatus.Unknown,
  					ServerName = settings.Name,
  					EnableSingleplayer = settings.EnableSingleplayer || Type != ServerType.Dedicated,
  					EnableSyncReports = settings.EnableSyncReports,
  					GameUid = Guid.NewGuid().ToString(),
  					Dedicated = Type == ServerType.Dedicated
  				}
  			};
  
  			if (Settings.RecordReplays && Type == ServerType.Dedicated)
  			{
  				recorder = new ReplayRecorder(() => { return Game.TimestampedFilename(extra: "-Server"); });
  
  				// We only need one handshake to initialize the replay.
  				// Add it now, then ignore the redundant handshakes from each client
  				RecordFakeHandshake();
  			}
  
  			new Thread(_ =>
  			{
  				// Initial status is set off the main thread to avoid triggering a load screen when joining a skirmish game
  				LobbyInfo.GlobalSettings.MapStatus = MapStatusCache[Map];
  				foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
  					t.ServerStarted(this);
  
  				Log.Write("server", "Initial mod: {0}", ModData.Manifest.Id);
  				Log.Write("server", "Initial map: {0}", LobbyInfo.GlobalSettings.Map);
  
  				while (true)
  				{
  					if (State != ServerState.ShuttingDown)
  					{
  						if (events.TryTake(out var e, 1000))
  							e.Invoke(this);
  
  						// PERF: Dedicated servers need to drain the action queue to remove references blocking the GC from cleaning up disposed objects.
  						if (Type == ServerType.Dedicated)
  							Game.PerformDelayedActions();
  
  						foreach (var t in serverTraits.WithInterface<ITick>())
  							t.Tick(this);
  					}
  
  					if (State == ServerState.ShuttingDown)
  					{
  						EndGame();
  						if (type != ServerType.Local)
  							Nat.TryRemovePortForward();
  						break;
  					}
  				}
  
  				foreach (var t in serverTraits.WithInterface<INotifyServerShutdown>())
  					t.ServerShutdown(this);
  
  				Conns.Clear();
  			})
  			{ IsBackground = true }.Start();
  		}
  
  		int nextPlayerIndex;
  		public int ChooseFreePlayerIndex()
  		{
  			return nextPlayerIndex++;
  		}
  
  		void OnClientPacket(Connection conn, int frame, byte[] data)
  		{
  			events.Add(new ClientPacketEvent(conn, frame, data));
  		}
  
  		void OnClientDisconnect(Connection conn)
  		{
  			events.Add(new ClientDisconnectEvent(conn));
  		}
  
  		void AcceptConnection(Socket socket)
  		{
  			if (State != ServerState.WaitingPlayers)
  				return;
  
  			// Validate player identity by asking them to sign a random blob of data
  			// which we can then verify against the player public key database
  			var token = Convert.ToBase64String(OpenRA.Exts.MakeArray(256, _ => (byte)Random.Next()));
  
  			var newConn = new Connection(socket, ChooseFreePlayerIndex(), token, OnClientPacket, OnClientDisconnect);
  			try
  			{
  				// Send handshake and client index.
  				var ms = new MemoryStream(8);
  				ms.WriteArray(BitConverter.GetBytes(ProtocolVersion.Handshake));
  				ms.WriteArray(BitConverter.GetBytes(newConn.PlayerIndex));
  				newConn.SendData(ms.ToArray());
  
  				// Dispatch a handshake order
  				var request = new HandshakeRequest
  				{
  					Mod = ModData.Manifest.Id,
  					Version = ModData.Manifest.Metadata.Version,
  					AuthToken = token
  				};
  
  				DispatchOrdersToClient(newConn, 0, 0, new Order("HandshakeRequest", null, false)
  				{
  					Type = OrderType.Handshake,
  					IsImmediate = true,
  					TargetString = request.Serialize()
  				}.Serialize());
  			}
  			catch (Exception e)
  			{
  				Log.Write("server", $"Handshake for client {newConn.EndPoint} failed: {e}");
  			}
  
  			Conns.Add(newConn);
  		}
  
  		void ValidateClient(Connection newConn, string data)
  		{
  			try
  			{
  				if (State == ServerState.GameStarted)
  				{
  					Log.Write("server", "Rejected connection from {0}; game is already started.", newConn.EndPoint);
  
  					SendOrderTo(newConn, "ServerError", "The game has already started");
  					DropClient(newConn);
  					return;
  				}
  
  				var handshake = HandshakeResponse.Deserialize(data);
  
  				if (!string.IsNullOrEmpty(Settings.Password) && handshake.Password != Settings.Password)
  				{
  					var message = string.IsNullOrEmpty(handshake.Password) ? "Server requires a password" : "Incorrect password";
  					SendOrderTo(newConn, "AuthenticationError", message);
  					DropClient(newConn);
  					return;
  				}
  
  				var ipAddress = ((IPEndPoint)newConn.EndPoint).Address;
  				var client = new Session.Client
  				{
  					Name = OpenRA.Settings.SanitizedPlayerName(handshake.Client.Name),
  					IPAddress = ipAddress.ToString(),
  					AnonymizedIPAddress = Type != ServerType.Local && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
  					Location = GeoIP.LookupCountry(ipAddress),
  					Index = newConn.PlayerIndex,
  					PreferredColor = handshake.Client.PreferredColor,
  					Color = handshake.Client.Color,
  					Faction = "Random",
  					SpawnPoint = 0,
  					Team = 0,
  					Handicap = 0,
  					State = Session.ClientState.Invalid,
  				};
  
  				if (ModData.Manifest.Id != handshake.Mod)
  				{
  					Log.Write("server", "Rejected connection from {0}; mods do not match.",
  						newConn.EndPoint);
  
  					SendOrderTo(newConn, "ServerError", "Server is running an incompatible mod");
  					DropClient(newConn);
  					return;
  				}
  
  				if (ModData.Manifest.Metadata.Version != handshake.Version)
  				{
  					Log.Write("server", "Rejected connection from {0}; Not running the same version.", newConn.EndPoint);
  
  					SendOrderTo(newConn, "ServerError", "Server is running an incompatible version");
  					DropClient(newConn);
  					return;
  				}
  
  				if (handshake.OrdersProtocol != ProtocolVersion.Orders)
  				{
  					Log.Write("server", "Rejected connection from {0}; incompatible Orders protocol version {1}.",
  						newConn.EndPoint, handshake.OrdersProtocol);
  
  					SendOrderTo(newConn, "ServerError", "Server is running an incompatible protocol");
  					DropClient(newConn);
  					return;
  				}
  
  				// Check if IP is banned
  				var bans = Settings.Ban.Union(TempBans);
  				if (bans.Contains(client.IPAddress))
  				{
  					Log.Write("server", "Rejected connection from {0}; Banned.", newConn.EndPoint);
  					SendOrderTo(newConn, "ServerError", $"You have been {(Settings.Ban.Contains(client.IPAddress) ? "banned" : "temporarily banned")} from the server");
  					DropClient(newConn);
  					return;
  				}
  
  				Action completeConnection = () =>
  				{
  					lock (LobbyInfo)
  					{
  						client.Slot = LobbyInfo.FirstEmptySlot();
  						client.IsAdmin = !LobbyInfo.Clients.Any(c1 => c1.IsAdmin);
  
  						if (client.IsObserver && !LobbyInfo.GlobalSettings.AllowSpectators)
  						{
  							SendOrderTo(newConn, "ServerError", "The game is full");
  							DropClient(newConn);
  							return;
  						}
  
  						if (client.Slot != null)
  							SyncClientToPlayerReference(client, Map.Players.Players[client.Slot]);
  						else
  							client.Color = Color.White;
  
  						// Promote connection to a valid client
  						LobbyInfo.Clients.Add(client);
  						newConn.Validated = true;
  
  						var clientPing = new Session.ClientPing { Index = client.Index };
  						LobbyInfo.ClientPings.Add(clientPing);
  
  						Log.Write("server", "Client {0}: Accepted connection from {1}.", newConn.PlayerIndex, newConn.EndPoint);
  
  						if (client.Fingerprint != null)
  							Log.Write("server", "Client {0}: Player fingerprint is {1}.", newConn.PlayerIndex, client.Fingerprint);
  
  
gt: 						foreach (var t in serverTraits.WithInterface<IClientJoined>())