diff --git a/FsmSync.cs b/FsmSync.cs
index d862b1a..d285379 100644
--- a/FsmSync.cs
+++ b/FsmSync.cs
@@ -292,6 +292,11 @@ namespace MSCO
if (Debug) MscoLog.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]");
return;
}
+ if (!Allowed(fsm, msg))
+ {
+ if (Debug) MscoLog.Warning("[MSCO] << refused: " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
+ return;
+ }
if (msg.EventName == VehicleAdmin.InstallEvent) { VehicleAdmin.InstallRemote(fsm); return; } // Admin hat ein Teil eingebaut
FsmState state = fsm.Fsm.GetState(msg.State);
if (state == null || switchState == null) return;
@@ -304,6 +309,35 @@ namespace MSCO
finally { applying--; }
}
+ ///
+ /// Darf ein Mitspieler diese FSM in diesen Zustand schalten? Sonst könnte ein manipulierter Client beliebige
+ /// Zustände bei allen anderen auslösen (z.B. Tod oder Speichern). Nie: der eigene Spieler (PLAYER), Systeme
+ /// (Tod, Speichern ...), MSCO selbst. Klicks nur auf FSMs, die auf einen Klick warten; Schrauben/Montage nur
+ /// über einen Übergang, den die FSM für genau dieses Event auch hat.
+ ///
+ static bool Allowed(PlayMakerFSM fsm, FsmEventMsg msg)
+ {
+ try
+ {
+ string root = fsm.transform.root.name;
+ if (root == "PLAYER" || root == "Systems" || root.StartsWith("MSCO_")) return false;
+ if (msg.EventName == VehicleAdmin.InstallEvent) return fsm.FsmName == "Assembly";
+ if (msg.EventName == "CLICK")
+ {
+ foreach (FsmState s in fsm.FsmStates) if (ChecksInput(s)) return fsm.Fsm.GetState(msg.State) != null;
+ return false;
+ }
+ if (Array.IndexOf(SyncedEvents, msg.EventName) < 0) return false;
+ foreach (FsmState s in fsm.FsmStates)
+ foreach (FsmTransition t in s.Transitions)
+ if (t.EventName == msg.EventName && t.ToState == msg.State) return true;
+ foreach (FsmTransition t in fsm.Fsm.GlobalTransitions)
+ if (t.EventName == msg.EventName && t.ToState == msg.State) return true;
+ return false;
+ }
+ catch { return false; }
+ }
+
/// FSM in einen Zustand schalten, ohne dass das als eigene Aktion erkannt und gesendet wird.
public static void SwitchStateSilently(PlayMakerFSM fsm, FsmState state)
{
diff --git a/MSCO.csproj b/MSCO.csproj
index 4fe408c..b162b37 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -99,6 +99,7 @@
+
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index f0d1d5b..ab5ef6d 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -1731,7 +1731,13 @@ namespace MSCO
string modId = r.ReadString();
int len = r.ReadInt32();
if (len < 0 || len > MaxModMessageBytes) return;
- if (modId == WorldAdmin.MessageId) { WorldAdmin.Receive(r.ReadBytes(len)); return; } // Admin: Uhrzeit/Wetter
+ if (modId == WorldAdmin.MessageId)
+ {
+ // Admin: Uhrzeit/Wetter. Der dedizierte Server lässt das nur von Admins durch; im P2P-Spiel nur vom Host
+ if (dedicated || from == NetSession.HostId) WorldAdmin.Receive(r.ReadBytes(len));
+ return;
+ }
+ if (modId.StartsWith("msco.")) return; // reserviert für MSCO selbst
ModLoader.Message(modId, from, r.ReadBytes(len));
return;
}
@@ -1740,7 +1746,8 @@ namespace MSCO
string text = r.ReadString();
PlayerInfo sender;
if (dedicated && from == NetSession.HostId) overlays.AddChat("", text, true); // Server/Admin-Meldung
- else if (text.StartsWith("* ")) overlays.AddChat("", text.Substring(2), true);
+ // "* ..." = Meldung (z.B. Admin-Aktion): auf dem dedizierten Server filtert der Server, im P2P-Spiel nur vom Host
+ else if (text.StartsWith("* ") && (dedicated || from == NetSession.HostId)) overlays.AddChat("", text.Substring(2), true);
else overlays.AddChat(players.TryGetValue(from, out sender) ? sender.Name : "Player " + from, text);
return;
}
@@ -2368,7 +2375,7 @@ namespace MSCO
///
System.Collections.IEnumerator ApplyWorldSnapshot(byte[] snapshot)
{
- var r = new BinaryReader(new MemoryStream(snapshot));
+ var r = new PacketReader(snapshot);
if (r.ReadBoolean())
{
@@ -2401,7 +2408,7 @@ namespace MSCO
for (int i = 0; i < eventCount; i++)
{
byte[] payload = r.ReadBytes(r.ReadInt32());
- FsmSync.Apply(FsmEventMsg.Deserialize(new BinaryReader(new MemoryStream(payload))));
+ FsmSync.Apply(FsmEventMsg.Deserialize(new PacketReader(payload)));
if ((i + 1) % SnapshotEventsPerFrame == 0) yield return null; // nicht alles in einem Frame
}
@@ -2426,7 +2433,7 @@ namespace MSCO
for (int i = 0; i < spawns; i++)
{
byte[] payload = r.ReadBytes(r.ReadInt32());
- try { if (ApplySpawn(new BinaryReader(new MemoryStream(payload)))) spawned++; }
+ try { if (ApplySpawn(new PacketReader(payload))) spawned++; }
catch (System.Exception e) { MscoLog.Warning("Could not spawn a saved item: " + e.Message); }
}
}
@@ -2439,7 +2446,7 @@ namespace MSCO
for (int i = 0; i < appeared; i++)
{
byte[] payload = r.ReadBytes(r.ReadInt32());
- try { ApplyAppeared(new BinaryReader(new MemoryStream(payload))); }
+ try { ApplyAppeared(new PacketReader(payload)); }
catch (System.Exception e) { MscoLog.Warning("Could not restore a bought item: " + e.Message); }
}
}
diff --git a/NetSession.cs b/NetSession.cs
index 891b6b1..b5aee3f 100644
--- a/NetSession.cs
+++ b/NetSession.cs
@@ -67,7 +67,7 @@ namespace MSCO
public BinaryReader Reader()
{
- return new BinaryReader(new MemoryStream(Payload));
+ return new PacketReader(Payload);
}
}
diff --git a/PacketReader.cs b/PacketReader.cs
new file mode 100644
index 0000000..006cc61
--- /dev/null
+++ b/PacketReader.cs
@@ -0,0 +1,37 @@
+using System.IO;
+
+namespace MSCO
+{
+ ///
+ /// BinaryReader für Daten aus dem Netz (Server, Mitspieler). Mono legt bei ReadBytes(n) und ReadString() vorab ein
+ /// Array bzw. einen StringBuilder in der angegebenen Länge an - eine gefälschte Länge (z.B. 2 Milliarden) ließ das
+ /// Spiel mit "Out of Memory" abstürzen. Hier wird jede Länge gegen die tatsächlich vorhandenen Bytes geprüft.
+ ///
+ public sealed class PacketReader : BinaryReader
+ {
+ public PacketReader(byte[] data) : base(new MemoryStream(data ?? new byte[0], false)) { }
+
+ long Remaining { get { return BaseStream.Length - BaseStream.Position; } }
+
+ public override byte[] ReadBytes(int count)
+ {
+ if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count);
+ return base.ReadBytes(count);
+ }
+
+ public override char[] ReadChars(int count)
+ {
+ if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count);
+ return base.ReadChars(count);
+ }
+
+ public override string ReadString()
+ {
+ long start = BaseStream.Position;
+ int length = Read7BitEncodedInt();
+ if (length < 0 || length > Remaining) throw new EndOfStreamException("Invalid string length " + length);
+ BaseStream.Position = start;
+ return base.ReadString();
+ }
+ }
+}
diff --git a/ServerBrowser.cs b/ServerBrowser.cs
index 4fd6429..57f1c88 100644
--- a/ServerBrowser.cs
+++ b/ServerBrowser.cs
@@ -244,7 +244,7 @@ namespace MSCO
byte[] payload = ReadExactly(stream, length - 1);
info.Ping = (int)watch.ElapsedMilliseconds;
- using (var r = new BinaryReader(new MemoryStream(payload)))
+ using (var r = new PacketReader(payload))
{
info.Name = r.ReadString();
info.Description = r.ReadString();
diff --git a/WorldAdmin.cs b/WorldAdmin.cs
index cdba227..eb692c7 100644
--- a/WorldAdmin.cs
+++ b/WorldAdmin.cs
@@ -204,7 +204,7 @@ namespace MSCO
public static void Receive(byte[] data)
{
if (!MenuHelper.InGame || data == null || data.Length < 17) return;
- using (var r = new BinaryReader(new MemoryStream(data)))
+ using (var r = new PacketReader(data))
{
byte kind = r.ReadByte();
int value = r.ReadInt32();