void InterpretServerOrder(Connection conn, Order o) { lock (LobbyInfo) { // Only accept handshake responses from unvalidated clients // Anything else may be an attempt to exploit the server if (!conn.Validated) { if (o.OrderString == "HandshakeResponse") { ValidateClient(conn, o.TargetString); } else { Log.Write("server", "Rejected connection from {0}; Order `{1}` is not a `HandshakeResponse`.", conn.Socket.RemoteEndPoint, o.OrderString); DropClient(conn); } return; } switch (o.OrderString) { case "Command": { var handledBy = serverTraits.WithInterface <IInterpretCommand>() .FirstOrDefault(t => t.InterpretCommand(this, conn, GetClient(conn), o.TargetString)); if (handledBy == null) { Log.Write("server", "Unknown server command: {0}", o.TargetString); SendOrderTo(conn, "Message", "Unknown server command: {0}".F(o.TargetString)); } break; } case "Chat": DispatchOrdersToClients(conn, 0, o.Serialize()); break; case "Pong": { if (!OpenRA.Exts.TryParseInt64Invariant(o.TargetString, out var pingSent)) { Log.Write("server", "Invalid order pong payload: {0}", o.TargetString); break; } var client = GetClient(conn); if (client == null) { return; } var pingFromClient = LobbyInfo.PingFromClient(client); if (pingFromClient == null) { return; } var history = pingFromClient.LatencyHistory.ToList(); history.Add(Game.RunTime - pingSent); // Cap ping history at 5 values (25 seconds) if (history.Count > 5) { history.RemoveRange(0, history.Count - 5); } pingFromClient.Latency = history.Sum() / history.Count; pingFromClient.LatencyJitter = (history.Max() - history.Min()) / 2; pingFromClient.LatencyHistory = history.ToArray(); SyncClientPing(); break; } case "GameSaveTraitData": { if (GameSave != null) { var data = MiniYaml.FromString(o.TargetString)[0]; GameSave.AddTraitData(int.Parse(data.Key), data.Value); } break; } case "CreateGameSave": { if (GameSave != null) { // Sanitize potentially malicious input var filename = o.TargetString; var invalidIndex = -1; var invalidChars = Path.GetInvalidFileNameChars(); while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1) { filename = filename.Remove(invalidIndex, 1); } var baseSavePath = Platform.ResolvePath( Platform.SupportDirPrefix, "Saves", ModData.Manifest.Id, ModData.Manifest.Metadata.Version); if (!Directory.Exists(baseSavePath)) { Directory.CreateDirectory(baseSavePath); } GameSave.Save(Path.Combine(baseSavePath, filename)); DispatchOrdersToClients(null, 0, Order.FromTargetString("GameSaved", filename, true).Serialize()); } break; } case "LoadGameSave": { if (Type == ServerType.Dedicated || State >= ServerState.GameStarted) { break; } // Sanitize potentially malicious input var filename = o.TargetString; var invalidIndex = -1; var invalidChars = Path.GetInvalidFileNameChars(); while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1) { filename = filename.Remove(invalidIndex, 1); } var savePath = Platform.ResolvePath( Platform.SupportDirPrefix, "Saves", ModData.Manifest.Id, ModData.Manifest.Metadata.Version, filename); GameSave = new GameSave(savePath); LobbyInfo.GlobalSettings = GameSave.GlobalSettings; LobbyInfo.Slots = GameSave.Slots; // Reassign clients to slots // - Bot ordering is preserved // - Humans are assigned on a first-come-first-serve basis // - Leftover humans become spectators // Start by removing all bots and assigning all players as spectators foreach (var c in LobbyInfo.Clients) { if (c.Bot != null) { LobbyInfo.Clients.Remove(c); var ping = LobbyInfo.PingFromClient(c); if (ping != null) { LobbyInfo.ClientPings.Remove(ping); } } else { c.Slot = null; } } // Rebuild/remap the saved client state // TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index; foreach (var kv in GameSave.SlotClients) { if (kv.Value.Bot != null) { var bot = new Session.Client() { Index = ChooseFreePlayerIndex(), State = Session.ClientState.NotReady, BotControllerClientIndex = adminClientIndex }; kv.Value.ApplyTo(bot); LobbyInfo.Clients.Add(bot); } else { // This will throw if the server doesn't have enough human clients to fill all player slots // See TODO above - this isn't a problem in practice because MP saves won't use this var client = LobbyInfo.Clients.First(c => c.Slot == null); kv.Value.ApplyTo(client); } } SyncLobbyInfo(); SyncLobbyClients(); SyncClientPing(); break; } } } }