diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e7cbc41 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +/Server/bin/ +/Server/obj/ +/Shared/bin/ +/Shared/obj/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 74ffcb5..9b581b3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,3 +73,35 @@ jobs: platforms : linux/amd64,linux/arm/v7,linux/arm64/v8 cache-from : type=gha,scope=${{ github.workflow }} cache-to : type=gha,scope=${{ github.workflow }},mode=max + - + name: Build binary files + run: | + ./docker-build.sh all + - + name : Upload Server + uses : actions/upload-artifact@v3 + with: + name : Server + path : ./bin/Server + if-no-files-found : error + - + name : Upload Server.arm + uses : actions/upload-artifact@v3 + with: + name : Server.arm + path : ./bin/Server.arm + if-no-files-found : error + - + name : Upload Server.arm64 + uses : actions/upload-artifact@v3 + with: + name : Server.arm64 + path : ./bin/Server.arm64 + if-no-files-found : error + - + name : Upload Server.exe + uses : actions/upload-artifact@v3 + with: + name : Server.exe + path : ./bin/Server.exe + if-no-files-found : error diff --git a/.gitignore b/.gitignore index 64e0e6f..2baf885 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ riderModule.iml .idea/ settings.json .vs/ + +/cache/ +/data/ diff --git a/Server/BanLists.cs b/Server/BanLists.cs new file mode 100644 index 0000000..3f16a61 --- /dev/null +++ b/Server/BanLists.cs @@ -0,0 +1,359 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +using Shared; +using Shared.Packet.Packets; + +namespace Server; + +using MUCH = Func failToFind, HashSet toActUpon, List<(string arg, IEnumerable amb)> ambig)>; + +public static class BanLists { + public static bool Enabled { + get { + return Settings.Instance.BanList.Enabled; + } + private set { + Settings.Instance.BanList.Enabled = value; + } + } + + private static ISet IPs { + get { + return Settings.Instance.BanList.IpAddresses; + } + } + + private static ISet Profiles { + get { + return Settings.Instance.BanList.Players; + } + } + + private static ISet Stages { + get { + return Settings.Instance.BanList.Stages; + } + } + + + private static bool IsIPv4(string str) { + return IPAddress.TryParse(str, out IPAddress? ip) + && ip != null + && ip.AddressFamily == AddressFamily.InterNetwork; + ; + } + + + public static bool IsIPv4Banned(Client user) { + IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint; + if (ipv4 == null) { return false; } + return IsIPv4Banned(ipv4.Address); + } + public static bool IsIPv4Banned(IPAddress ipv4) { + return IsIPv4Banned(ipv4.ToString()); + } + public static bool IsIPv4Banned(string ipv4) { + return IPs.Contains(ipv4); + } + + public static bool IsProfileBanned(Client user) { + return IsProfileBanned(user.Id); + } + public static bool IsProfileBanned(string str) { + if (!Guid.TryParse(str, out Guid id)) { return false; } + return IsProfileBanned(id); + } + public static bool IsProfileBanned(Guid id) { + return Profiles.Contains(id); + } + + public static bool IsStageBanned(string stage) { + return Stages.Contains(stage); + } + + public static bool IsClientBanned(Client user) { + return IsProfileBanned(user) || IsIPv4Banned(user); + } + + + private static void BanIPv4(Client user) { + IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint; + if (ipv4 != null) { + BanIPv4(ipv4.Address); + } + } + private static void BanIPv4(IPAddress ipv4) { + BanIPv4(ipv4.ToString()); + } + private static void BanIPv4(string ipv4) { + IPs.Add(ipv4); + } + + private static void BanProfile(Client user) { + BanProfile(user.Id); + } + private static void BanProfile(string str) { + if (!Guid.TryParse(str, out Guid id)) { return; } + BanProfile(id); + } + private static void BanProfile(Guid id) { + Profiles.Add(id); + } + + private static void BanStage(string stage) { + Stages.Add(stage); + } + + private static void BanClient(Client user) { + BanProfile(user); + BanIPv4(user); + } + + + private static void UnbanIPv4(Client user) { + IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint; + if (ipv4 != null) { + UnbanIPv4(ipv4.Address); + } + } + private static void UnbanIPv4(IPAddress ipv4) { + UnbanIPv4(ipv4.ToString()); + } + private static void UnbanIPv4(string ipv4) { + IPs.Remove(ipv4); + } + + private static void UnbanProfile(Client user) { + UnbanProfile(user.Id); + } + private static void UnbanProfile(string str) { + if (!Guid.TryParse(str, out Guid id)) { return; } + UnbanProfile(id); + } + private static void UnbanProfile(Guid id) { + Profiles.Remove(id); + } + + private static void UnbanStage(string stage) { + Stages.Remove(stage); + } + + + private static void Save() { + Settings.SaveSettings(true); + } + + + public static void Crash( + Client user, + int delay_ms = 0 + ) { + user.Ignored = true; + Task.Run(async () => { + if (delay_ms > 0) { + await Task.Delay(delay_ms); + } + bool permanent = user.Banned; + await user.Send(new ChangeStagePacket { + Id = (permanent ? "$agogus/ban4lyfe" : "$among$us/cr4sh%"), + Stage = (permanent ? "$ejected" : "$agogusStage"), + Scenario = (sbyte) (permanent ? 69 : 21), + SubScenarioType = (byte) (permanent ? 21 : 69), + }); + }); + } + + private static void CrashMultiple(string[] args, MUCH much) { + foreach (Client user in much(args).toActUpon) { + user.Banned = true; + Crash(user); + } + } + + + public static string HandleBanCommand(string[] args, MUCH much) { + if (args.Length == 0) { + return "Usage: ban {list|enable|disable|player|profile|ip|stage} ..."; + } + + string cmd = args[0]; + args = args.Skip(1).ToArray(); + + switch (cmd) { + default: + return "Usage: ban {list|enable|disable|player|profile|ip|stage} ..."; + + case "list": + if (args.Length != 0) { + return "Usage: ban list"; + } + StringBuilder list = new StringBuilder(); + list.Append("BanList: " + (Enabled ? "enabled" : "disabled")); + + if (IPs.Count > 0) { + list.Append("\nBanned IPv4 addresses:\n- "); + list.Append(string.Join("\n- ", IPs)); + } + + if (Profiles.Count > 0) { + list.Append("\nBanned profile IDs:\n- "); + list.Append(string.Join("\n- ", Profiles)); + } + + if (Stages.Count > 0) { + list.Append("\nBanned stages:\n- "); + list.Append(string.Join("\n- ", Stages)); + } + + return list.ToString(); + + case "enable": + if (args.Length != 0) { + return "Usage: ban enable"; + } + Enabled = true; + Save(); + return "BanList enabled."; + + case "disable": + if (args.Length != 0) { + return "Usage: ban disable"; + } + Enabled = false; + Save(); + return "BanList disabled."; + + case "player": + if (args.Length == 0) { + return "Usage: ban player <* | !* (usernames to not ban...) | (usernames to ban...)>"; + } + + var res = much(args); + + StringBuilder sb = new StringBuilder(); + sb.Append(res.toActUpon.Count > 0 ? "Banned players: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : ""); + sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : ""); + if (res.ambig.Count > 0) { + res.ambig.ForEach(x => { + sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}"); + }); + } + + foreach (Client user in res.toActUpon) { + user.Banned = true; + BanClient(user); + Crash(user); + } + + Save(); + return sb.ToString(); + + case "profile": + if (args.Length != 1) { + return "Usage: ban profile "; + } + if (!Guid.TryParse(args[0], out Guid id)) { + return "Invalid profile ID value!"; + } + if (IsProfileBanned(id)) { + return "Profile " + id.ToString() + " is already banned."; + } + BanProfile(id); + CrashMultiple(args, much); + Save(); + return "Banned profile: " + id.ToString(); + + case "ip": + if (args.Length != 1) { + return "Usage: ban ip "; + } + if (!IsIPv4(args[0])) { + return "Invalid IPv4 address!"; + } + if (IsIPv4Banned(args[0])) { + return "IP " + args[0] + " is already banned."; + } + BanIPv4(args[0]); + CrashMultiple(args, much); + Save(); + return "Banned ip: " + args[0]; + + case "stage": + if (args.Length != 1) { + return "Usage: ban stage "; + } + string? stage = Shared.Stages.Input2Stage(args[0]); + if (stage == null) { + return "Invalid stage name!"; + } + if (IsStageBanned(stage)) { + return "Stage " + stage + " is already banned."; + } + var stages = Shared.Stages + .StagesByInput(args[0]) + .Where(s => !IsStageBanned(s)) + .ToList() + ; + foreach (string s in stages) { + BanStage(s); + } + Save(); + return "Banned stage: " + string.Join(", ", stages); + } + } + + + public static string HandleUnbanCommand(string[] args) { + if (args.Length != 2) { + return "Usage: unban {profile|ip|stage} "; + } + + string cmd = args[0]; + string val = args[1]; + + switch (cmd) { + default: + return "Usage: unban {profile|ip|stage} "; + + case "profile": + if (!Guid.TryParse(val, out Guid id)) { + return "Invalid profile ID value!"; + } + if (!IsProfileBanned(id)) { + return "Profile " + id.ToString() + " is not banned."; + } + UnbanProfile(id); + Save(); + return "Unbanned profile: " + id.ToString(); + + case "ip": + if (!IsIPv4(val)) { + return "Invalid IPv4 address!"; + } + if (!IsIPv4Banned(val)) { + return "IP " + val + " is not banned."; + } + UnbanIPv4(val); + Save(); + return "Unbanned ip: " + val; + + case "stage": + string stage = Shared.Stages.Input2Stage(val) ?? val; + if (!IsStageBanned(stage)) { + return "Stage " + stage + " is not banned."; + } + var stages = Shared.Stages + .StagesByInput(val) + .Where(IsStageBanned) + .ToList() + ; + foreach (string s in stages) { + UnbanStage(s); + } + Save(); + return "Unbanned stage: " + string.Join(", ", stages); + } + } +} diff --git a/Server/Client.cs b/Server/Client.cs index 5f3336f..8e7ba2d 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -12,6 +12,8 @@ namespace Server; public class Client : IDisposable { public readonly ConcurrentDictionary Metadata = new ConcurrentDictionary(); // can be used to store any information about a player public bool Connected = false; + public bool Ignored = false; + public bool Banned = false; public CostumePacket? CurrentCostume = null; // required for proper client sync public string Name { get => Logger.Name; @@ -40,8 +42,9 @@ public Client(Client other, Socket socket) { } public void Dispose() { - if (Socket?.Connected is true) + if (Socket?.Connected is true) { Socket.Disconnect(false); + } } @@ -50,9 +53,14 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)]; try { + // don't send most packets to ignored players + if (Ignored && packetAttribute.Type != PacketType.Init && packetAttribute.Type != PacketType.ChangeStage) { + memory.Dispose(); + return; + } Server.FillPacket(new PacketHeader { - Id = sender?.Id ?? Id, - Type = packetAttribute.Type, + Id = sender?.Id ?? Id, + Type = packetAttribute.Type, PacketSize = packet.Size }, packet, memory.Memory); } @@ -68,14 +76,42 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa public async Task Send(Memory data, Client? sender) { PacketHeader header = new PacketHeader(); header.Deserialize(data.Span); - if (!Connected && header.Type is not PacketType.Connect) { + + if (!Connected && !Ignored && header.Type != PacketType.Connect) { Server.Logger.Error($"Didn't send {header.Type} to {Id} because they weren't connected yet"); return; } + // don't send most packets to ignored players + if (Ignored && header.Type != PacketType.Init && header.Type != PacketType.ChangeStage) { + return; + } + await Socket!.SendAsync(data[..(Constants.HeaderSize + header.PacketSize)], SocketFlags.None); } + public void CleanMetadataOnNewConnection() { + object? tmp; + Metadata.TryRemove("time", out tmp); + Metadata.TryRemove("seeking", out tmp); + Metadata.TryRemove("lastCostumePacket", out tmp); + Metadata.TryRemove("lastCapturePacket", out tmp); + Metadata.TryRemove("lastGamePacket", out tmp); + Metadata.TryRemove("lastPlayerPacket", out tmp); + } + + public TagPacket? GetTagPacket() { + var time = (Time?) (this.Metadata.ContainsKey("time") ? this.Metadata["time"] : null); + var seek = (bool?) (this.Metadata.ContainsKey("seeking") ? this.Metadata["seeking"] : null); + if (time == null && seek == null) { return null; } + return new TagPacket { + UpdateType = (seek != null ? TagPacket.TagUpdate.State : 0) | (time != null ? TagPacket.TagUpdate.Time: 0), + IsIt = seek ?? false, + Seconds = (byte) (time?.Seconds ?? 0), + Minutes = (ushort) (time?.Minutes ?? 0), + }; + } + public static bool operator ==(Client? left, Client? right) { return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id; } diff --git a/Server/Program.cs b/Server/Program.cs index 7e46b1e..a0a7dd3 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -62,23 +62,11 @@ async Task LoadShines() await LoadShines(); server.ClientJoined += (c, _) => { - if (Settings.Instance.BanList.Enabled - && (Settings.Instance.BanList.Players.Contains(c.Id) - || Settings.Instance.BanList.IpAddresses.Contains( - ((IPEndPoint) c.Socket!.RemoteEndPoint!).Address.ToString()))) - throw new Exception($"Banned player attempted join: {c.Name}"); c.Metadata["shineSync"] = new ConcurrentBag(); c.Metadata["loadedSave"] = false; c.Metadata["scenario"] = (byte?) 0; c.Metadata["2d"] = false; c.Metadata["speedrun"] = false; - foreach (Client client in server.ClientsConnected) { - try { - c.Send((GamePacket) client.Metadata["lastGamePacket"]!, client).Wait(); - } catch { - // lol who gives a fuck - } - } }; async Task ClientSyncShineBag(Client client) { @@ -86,7 +74,7 @@ async Task ClientSyncShineBag(Client client) { try { if ((bool?) client.Metadata["speedrun"] ?? false) return; ConcurrentBag clientBag = (ConcurrentBag) (client.Metadata["shineSync"] ??= new ConcurrentBag()); - foreach (int shine in shineBag.Except(clientBag).ToArray()) { + foreach (int shine in shineBag.Except(clientBag).Except(Settings.Instance.Shines.Excluded).ToArray()) { if (!client.Connected) return; await client.Send(new ShinePacket { ShineId = shine @@ -115,13 +103,50 @@ async void SyncShineBag() { float MarioSize(bool is2d) => is2d ? 180 : 160; +void flipPlayer(Client c, ref PlayerPacket pp) { + pp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); + pp.Rotation *= ( + Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) + * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI)) + ); +}; + +void logError(Task x) { + if (x.Exception != null) { + consoleLogger.Error(x.Exception.ToString()); + } +}; + server.PacketHandler = (c, p) => { switch (p) { case GamePacket gamePacket: { + // crash ignored player + if (c.Ignored) { + c.Logger.Info($"Crashing ignored player after entering stage {gamePacket.Stage}."); + BanLists.Crash(c, 500); + return false; + } + + // crash player entering a banned stage + if (BanLists.Enabled && BanLists.IsStageBanned(gamePacket.Stage)) { + c.Logger.Warn($"Crashing player for entering banned stage {gamePacket.Stage}."); + BanLists.Crash(c, 500); + return false; + } + c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}"); + + // reset lastPlayerPacket on stage changes + object? old = null; + c.Metadata.TryGetValue("lastGamePacket", out old); + if (old != null && ((GamePacket) old).Stage != gamePacket.Stage) { + c.Metadata["lastPlayerPacket"] = null; + } + c.Metadata["scenario"] = gamePacket.ScenarioNum; c.Metadata["2d"] = gamePacket.Is2d; c.Metadata["lastGamePacket"] = gamePacket; + switch (gamePacket.Stage) { case "CapWorldHomeStage" when gamePacket.ScenarioNum == 0: c.Metadata["speedrun"] = true; @@ -145,8 +170,7 @@ async void SyncShineBag() { server.BroadcastReplace(gamePacket, c, (from, to, gp) => { gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200; #pragma warning disable CS4014 - to.Send(gp, from) - .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); + to.Send(gp, from).ContinueWith(logError); #pragma warning restore CS4014 }); return false; @@ -155,24 +179,42 @@ async void SyncShineBag() { break; } + // ignore all other packets from ignored players + case IPacket pack when c.Ignored: { + return false; + } + case TagPacket tagPacket: { + // c.Logger.Info($"Got tag packet: {tagPacket.IsIt}"); if ((tagPacket.UpdateType & TagPacket.TagUpdate.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt; if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0) c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now); break; } - case CostumePacket costumePacket: + case CapturePacket capturePacket: { + // c.Logger.Info($"Got capture packet: {capturePacket.ModelName}"); + c.Metadata["lastCapturePacket"] = capturePacket; + break; + } + + case CostumePacket costumePacket: { c.Logger.Info($"Got costume packet: {costumePacket.BodyName}, {costumePacket.CapName}"); + c.Metadata["lastCostumePacket"] = costumePacket; c.CurrentCostume = costumePacket; #pragma warning disable CS4014 ClientSyncShineBag(c); //no point logging since entire def has try/catch #pragma warning restore CS4014 c.Metadata["loadedSave"] = true; break; + } case ShinePacket shinePacket: { if (!Settings.Instance.Shines.Enabled) return false; + if (Settings.Instance.Shines.Excluded.Contains(shinePacket.ShineId)) { + c.Logger.Info($"Got moon {shinePacket.ShineId} (excluded)"); + return false; + } if (c.Metadata["loadedSave"] is false) break; ConcurrentBag playerBag = (ConcurrentBag)c.Metadata["shineSync"]!; shineBag.Add(shinePacket.ShineId); @@ -183,33 +225,35 @@ async void SyncShineBag() { break; } - case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled - && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others - && Settings.Instance.Flip.Players.Contains(c.Id): { - playerPacket.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); - playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) - * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI)); + case PlayerPacket playerPacket: { + c.Metadata["lastPlayerPacket"] = playerPacket; + // flip for all + if ( Settings.Instance.Flip.Enabled + && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others + && Settings.Instance.Flip.Players.Contains(c.Id) + ) { + flipPlayer(c, ref playerPacket); #pragma warning disable CS4014 - server.Broadcast(playerPacket, c) - .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); + server.Broadcast(playerPacket, c).ContinueWith(logError); #pragma warning restore CS4014 - return false; - } - case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled - && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self - && !Settings.Instance.Flip.Players.Contains(c.Id): { - server.BroadcastReplace(playerPacket, c, (from, to, sp) => { - if (Settings.Instance.Flip.Players.Contains(to.Id)) { - sp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); - sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) - * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI)); - } + return false; + } + // flip only for specific clients + if ( Settings.Instance.Flip.Enabled + && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self + && !Settings.Instance.Flip.Players.Contains(c.Id) + ) { + server.BroadcastReplace(playerPacket, c, (from, to, sp) => { + if (Settings.Instance.Flip.Players.Contains(to.Id)) { + flipPlayer(c, ref sp); + } #pragma warning disable CS4014 - to.Send(sp, from) - .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); + to.Send(sp, from).ContinueWith(logError); #pragma warning restore CS4014 - }); - return false; + }); + return false; + } + break; } } @@ -220,38 +264,49 @@ async void SyncShineBag() { HashSet failToFind = new(); HashSet toActUpon; List<(string arg, IEnumerable amb)> ambig = new(); - if (args[0] == "*") + if (args[0] == "*") { toActUpon = new(server.Clients.Where(c => c.Connected)); + } else { toActUpon = args[0] == "!*" ? new(server.Clients.Where(c => c.Connected)) : new(); for (int i = (args[0] == "!*" ? 1 : 0); i < args.Length; i++) { string arg = args[i]; - IEnumerable search = server.Clients.Where(c => c.Connected && - (c.Name.ToLower().StartsWith(arg.ToLower()) || (Guid.TryParse(arg, out Guid res) && res == c.Id))); - if (!search.Any()) + IEnumerable search = server.Clients.Where(c => c.Connected && ( + c.Name.ToLower().StartsWith(arg.ToLower()) + || (Guid.TryParse(arg, out Guid res) && res == c.Id) + || (IPAddress.TryParse(arg, out IPAddress? ip) && ip.Equals(((IPEndPoint) c.Socket!.RemoteEndPoint!).Address)) + )); + if (!search.Any()) { failToFind.Add(arg); //none found + } else if (search.Count() > 1) { Client? exact = search.FirstOrDefault(x => x.Name == arg); if (!ReferenceEquals(exact, null)) { //even though multiple matches, since exact match, it isn't ambiguous - if (args[0] == "!*") + if (args[0] == "!*") { toActUpon.Remove(exact); - else + } + else { toActUpon.Add(exact); + } } else { - if (!ambig.Any(x => x.arg == arg)) + if (!ambig.Any(x => x.arg == arg)) { ambig.Add((arg, search.Select(x => x.Name))); //more than one match - foreach (var rem in search.ToList()) //need copy because can't remove from list while iterating over it + } + foreach (var rem in search.ToList()) { //need copy because can't remove from list while iterating over it toActUpon.Remove(rem); + } } } else { //only one match, so autocomplete - if (args[0] == "!*") + if (args[0] == "!*") { toActUpon.Remove(search.First()); - else + } + else { toActUpon.Add(search.First()); + } } } } @@ -298,54 +353,14 @@ async void SyncShineBag() { } foreach (Client user in res.toActUpon) { - Task.Run(async () => { - await user.Send(new ChangeStagePacket { - Id = "$among$us/SubArea", - Stage = "$agogusStage", - Scenario = 21, - SubScenarioType = 69 // invalid id - }); - user.Dispose(); - }); + BanLists.Crash(user); } return sb.ToString(); }); -CommandHandler.RegisterCommand("ban", args => { - if (args.Length == 0) { - return "Usage: ban <* | !* (usernames to not ban...) | (usernames to ban...)>"; - } - - var res = MultiUserCommandHelper(args); - - StringBuilder sb = new StringBuilder(); - sb.Append(res.toActUpon.Count > 0 ? "Banned: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : ""); - sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : ""); - if (res.ambig.Count > 0) { - res.ambig.ForEach(x => { - sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}"); - }); - } - - foreach (Client user in res.toActUpon) { - Task.Run(async () => { - await user.Send(new ChangeStagePacket { - Id = "$agogus/banned4lyfe", - Stage = "$ejected", - Scenario = 69, - SubScenarioType = 21 // invalid id - }); - IPEndPoint? endpoint = (IPEndPoint?) user.Socket?.RemoteEndPoint; - Settings.Instance.BanList.Players.Add(user.Id); - if (endpoint != null) Settings.Instance.BanList.IpAddresses.Add(endpoint.ToString()); - user.Dispose(); - }); - } - - Settings.SaveSettings(); - return sb.ToString(); -}); +CommandHandler.RegisterCommand("ban", args => { return BanLists.HandleBanCommand(args, (args) => MultiUserCommandHelper(args)); }); +CommandHandler.RegisterCommand("unban", args => { return BanLists.HandleUnbanCommand(args); }); CommandHandler.RegisterCommand("send", args => { const string optionUsage = "Usage: send "; @@ -562,12 +577,16 @@ await server.Broadcast(new TagPacket { }); CommandHandler.RegisterCommand("shine", args => { - const string optionUsage = "Valid options: list, clear, sync, send, set"; + const string optionUsage = "Valid options: list, clear, sync, send, set, include, exclude"; if (args.Length < 1) return optionUsage; switch (args[0]) { case "list" when args.Length == 1: - return $"Shines: {string.Join(", ", shineBag)}"; + return $"Shines: {string.Join(", ", shineBag)}" + ( + Settings.Instance.Shines.Excluded.Count() > 0 + ? "\nExcluded Shines: " + string.Join(", ", Settings.Instance.Shines.Excluded) + : "" + ); case "clear" when args.Length == 1: shineBag.Clear(); Task.Run(async () => { @@ -604,6 +623,21 @@ await c.Send(new ShinePacket { return optionUsage; } + case "exclude" when args.Length == 2: + case "include" when args.Length == 2: { + if (int.TryParse(args[1], out int sid)) { + if (args[0] == "exclude") { + Settings.Instance.Shines.Excluded.Add(sid); + Settings.SaveSettings(); + return $"Exclude shine {sid} from syncing."; + } else { + Settings.Instance.Shines.Excluded.Remove(sid); + Settings.SaveSettings(); + return $"No longer exclude shine {sid} from syncing."; + } + } + return optionUsage; + } default: return optionUsage; } @@ -651,7 +685,7 @@ await c.Send(new ShinePacket { } } } -}).ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); +}).ContinueWith(logError); #pragma warning restore CS4014 await server.Listen(cts.Token); diff --git a/Server/Server.cs b/Server/Server.cs index 30646d7..ef05d8a 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -30,6 +30,7 @@ public async Task Listen(CancellationToken? token = null) { Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}"); + // start sub thread to handle client try { #pragma warning disable CS4014 Task.Run(() => HandleSocket(socket)) @@ -63,7 +64,7 @@ public async Task Listen(CancellationToken? token = null) { public static void FillPacket(PacketHeader header, T packet, Memory memory) where T : struct, IPacket { Span data = memory.Span; - + header.Serialize(data[..Constants.HeaderSize]); packet.Serialize(data[Constants.HeaderSize..]); } @@ -72,27 +73,29 @@ public static void FillPacket(PacketHeader header, T packet, Memory mem public delegate void PacketReplacer(Client from, Client to, T value); // replacer must send public void BroadcastReplace(T packet, Client sender, PacketReplacer packetReplacer) where T : struct, IPacket { - foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) packetReplacer(sender, client, packet); + foreach (Client client in Clients.Where(c => c.Connected && !c.Ignored && sender.Id != c.Id)) { + packetReplacer(sender, client, packet); + } } public async Task Broadcast(T packet, Client sender) where T : struct, IPacket { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { - Id = sender?.Id ?? Guid.Empty, - Type = Constants.PacketMap[typeof(T)].Type, - PacketSize = packet.Size + Id = sender?.Id ?? Guid.Empty, + Type = Constants.PacketMap[typeof(T)].Type, + PacketSize = packet.Size, }; FillPacket(header, packet, memory.Memory); await Broadcast(memory, sender); } public Task Broadcast(T packet) where T : struct, IPacket { - return Task.WhenAll(Clients.Where(c => c.Connected).Select(async client => { + return Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored).Select(async client => { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { - Id = client.Id, - Type = Constants.PacketMap[typeof(T)].Type, - PacketSize = packet.Size + Id = client.Id, + Type = Constants.PacketMap[typeof(T)].Type, + PacketSize = packet.Size, }; FillPacket(header, packet, memory.Memory); await client.Send(memory.Memory, client); @@ -106,7 +109,7 @@ public Task Broadcast(T packet) where T : struct, IPacket { /// Memory owner to dispose once done /// Optional sender to not broadcast data to public async Task Broadcast(IMemoryOwner data, Client? sender = null) { - await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data.Memory, sender))); + await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data.Memory, sender))); data.Dispose(); } @@ -116,7 +119,7 @@ public async Task Broadcast(IMemoryOwner data, Client? sender = null) { /// Memory to send to the clients /// Optional sender to not broadcast data to public async void Broadcast(Memory data, Client? sender = null) { - await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data, sender))); + await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data, sender))); } public Client? FindExistingClient(Guid id) { @@ -128,9 +131,7 @@ private async void HandleSocket(Socket socket) { Client client = new Client(socket) {Server = this}; var remote = socket.RemoteEndPoint; IMemoryOwner memory = null!; - await client.Send(new InitPacket { - MaxPlayers = Settings.Instance.Server.MaxPlayers - }); + bool first = true; try { while (true) { @@ -153,8 +154,9 @@ async Task Read(Memory readMem, int readSize, int readOffset) { return true; } - if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) + if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) { break; + } PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]); Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize); if (header.PacketSize > 0) { @@ -162,54 +164,100 @@ async Task Read(Memory readMem, int readSize, int readOffset) { memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize); memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]); memTemp.Dispose(); - if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) + if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) { break; + } } // connection initialization if (first) { - first = false; - if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}"); + first = false; // only do this once + + // first client packet has to be the client init + if (header.Type != PacketType.Connect) { + throw new Exception($"First packet was not init, instead it was {header.Type} ({remote})"); + } ConnectPacket connect = new ConnectPacket(); connect.Deserialize(memory.Memory.Span[packetRange]); + + client.Id = header.Id; + client.Name = connect.ClientName; + + // is the IPv4 address banned? + if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { + Logger.Warn($"Ignoring banned IPv4 address for {client.Name} ({client.Id}/{remote})"); + client.Ignored = true; + client.Banned = true; + } + // is the profile ID banned? + else if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) { + client.Logger.Warn($"Ignoring banned profile ID for {client.Name} ({client.Id}/{remote})"); + client.Ignored = true; + client.Banned = true; + } + // is the server full? + else if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) { + client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}"); + client.Ignored = true; + } + + // send server init (required to crash ignored players later) + await client.Send(new InitPacket { + MaxPlayers = (client.Ignored ? (ushort) 1 : Settings.Instance.Server.MaxPlayers), + }); + + // don't init or announce an ignored client to other players any further + if (client.Ignored) { + memory.Dispose(); + continue; + } + + bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; + + // add client to the set of connected players lock (Clients) { - if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) { - client.Logger.Error($"Turned away as server is at max clients"); + // is the server full? (check again, to prevent race conditions) + if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) { + client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}"); + client.Ignored = true; memory.Dispose(); - goto disconnect; + continue; } - bool firstConn = true; + // detect and handle reconnections + bool isClientNew = true; switch (connect.ConnectionType) { case ConnectPacket.ConnectionTypes.FirstConnection: case ConnectPacket.ConnectionTypes.Reconnecting: { - client.Id = header.Id; - if (FindExistingClient(header.Id) is { } oldClient) { - firstConn = false; + if (FindExistingClient(client.Id) is { } oldClient) { + isClientNew = false; client = new Client(oldClient, socket); + client.Name = connect.ClientName; Clients.Remove(oldClient); Clients.Add(client); if (oldClient.Connected) { oldClient.Logger.Info($"Disconnecting already connected client {oldClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}"); oldClient.Dispose(); } - } else { + } + else { connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection; } break; } - default: - throw new Exception($"Invalid connection type {connect.ConnectionType}"); + default: { + throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})"); + } } - client.Name = connect.ClientName; client.Connected = true; - if (firstConn) { + + if (isClientNew) { // do any cleanup required when it comes to new clients - List toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null); - Clients.RemoveAll(c => c.Id == header.Id); + List toDisconnect = Clients.FindAll(c => c.Id == client.Id && c.Connected && c.Socket != null); + Clients.RemoveAll(c => c.Id == client.Id); client.Id = header.Id; Clients.Add(client); @@ -219,26 +267,35 @@ async Task Read(Memory readMem, int readSize, int readOffset) { ClientJoined?.Invoke(client, connect); } + // a known client reconnects, but with a new first connection (e.g. after a restart) + else if (wasFirst) { + client.CleanMetadataOnNewConnection(); + } } - List otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null); + // for all other clients that are already connected + List otherConnectedPlayers = Clients.FindAll(c => c.Id != client.Id && c.Connected && c.Socket != null); await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { IMemoryOwner tempBuffer = MemoryPool.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size)); + + // make the other client known to the new client PacketHeader connectHeader = new PacketHeader { - Id = other.Id, - Type = PacketType.Connect, - PacketSize = connect.Size + Id = other.Id, + Type = PacketType.Connect, + PacketSize = connect.Size, }; connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]); ConnectPacket connectPacket = new ConnectPacket { ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is - MaxPlayers = Settings.Instance.Server.MaxPlayers, - ClientName = other.Name + MaxPlayers = Settings.Instance.Server.MaxPlayers, + ClientName = other.Name, }; connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]); await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null); + + // tell the new client what costume the other client has if (other.CurrentCostume.HasValue) { - connectHeader.Type = PacketType.Costume; + connectHeader.Type = PacketType.Costume; connectHeader.PacketSize = other.CurrentCostume.Value.Size; connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]); other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + connectHeader.PacketSize)]); @@ -246,10 +303,19 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { } tempBuffer.Dispose(); + + // make the other client reset their puppet cache for this new client, if it is a new connection (after restart) + if (wasFirst) { + await SendEmptyPackets(client, other); + } }); - Logger.Info($"Client {client.Name} ({client.Id}/{socket.RemoteEndPoint}) connected."); - } else if (header.Id != client.Id && client.Id != Guid.Empty) { + Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected."); + + // send missing or outdated packets from others to the new client + await ResendPackets(client); + } + else if (header.Id != client.Id && client.Id != Guid.Empty) { throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}"); } @@ -262,9 +328,13 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { } try { + // parse the packet IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!; packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]); + + // process the packet if (PacketHandler?.Invoke(client, packet) is false) { + // don't broadcast the packet to everyone memory.Dispose(); continue; } @@ -272,7 +342,9 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { catch (Exception e) { client.Logger.Error($"Packet handler warning: {e}"); } + #pragma warning disable CS4014 + // broadcast the packet to everyone Broadcast(memory, client) .ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 @@ -281,7 +353,8 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { catch (Exception e) { if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) { client.Logger.Info($"Disconnected from the server: Connection reset"); - } else { + } + else { client.Logger.Error($"Disconnecting due to exception: {e}"); if (socket.Connected) { #pragma warning disable CS4014 @@ -294,10 +367,22 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { memory?.Dispose(); } +<<<<<<< HEAD disconnect: Logger.Info($"Client {socket.RemoteEndPoint} ({client.Name}/{client.Id}) disconnected from the server"); // Clients.Remove(client) +======= + // client disconnected + if (client.Name != "Unknown User" && client.Id != Guid.Parse("00000000-0000-0000-0000-000000000000")) { + Logger.Info($"Client {remote} ({client.Name}/{client.Id}) disconnected from the server"); + } + else { + Logger.Info($"Client {remote} disconnected from the server"); + } + + bool wasConnected = client.Connected; +>>>>>>> Sanae6-master client.Connected = false; try { client.Dispose(); @@ -310,6 +395,42 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { #pragma warning restore CS4014 } + private async Task ResendPackets(Client client) { + async Task trySendPack(Client other, T? packet) where T : struct, IPacket { + if (packet == null) { return; } + try { + await client.Send((T) packet, other); + } + catch { + // lol who gives a fuck + } + }; + async Task trySendMeta(Client other, string packetType) where T : struct, IPacket { + if (!other.Metadata.ContainsKey(packetType)) { return; } + await trySendPack(other, (T) other.Metadata[packetType]!); + }; + await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => { + if (client.Id == other.Id) { return; } + await trySendMeta(other, "lastCostumePacket"); + await trySendMeta(other, "lastCapturePacket"); + await trySendPack(other, other.GetTagPacket()); + await trySendMeta(other, "lastGamePacket"); + await trySendMeta(other, "lastPlayerPacket"); + }); + } + + private async Task SendEmptyPackets(Client client, Client other) { + await other.Send(new TagPacket { + UpdateType = TagPacket.TagUpdate.State | TagPacket.TagUpdate.Time, + IsIt = false, + Seconds = 0, + Minutes = 0, + }, client); + await other.Send(new CapturePacket { + ModelName = "", + }, client); + } + private static PacketHeader GetHeader(Span data) { //no need to error check, the client will disconnect when the packet is invalid :) PacketHeader header = new PacketHeader(); diff --git a/Server/Settings.cs b/Server/Settings.cs index fe075e5..f696a7d 100644 --- a/Server/Settings.cs +++ b/Server/Settings.cs @@ -30,10 +30,10 @@ public static void LoadSettings() { LoadHandler?.Invoke(); } - public static void SaveSettings() { + public static void SaveSettings(bool silent = false) { try { File.WriteAllText("settings.json", JsonConvert.SerializeObject(Instance, Formatting.Indented, new StringEnumConverter(new CamelCaseNamingStrategy()))); - Logger.Info("Saved settings to settings.json"); + if (!silent) { Logger.Info("Saved settings to settings.json"); } } catch (Exception e) { Logger.Error($"Failed to save settings.json {e}"); @@ -43,7 +43,7 @@ public static void SaveSettings() { public ServerTable Server { get; set; } = new ServerTable(); public FlipTable Flip { get; set; } = new FlipTable(); public ScenarioTable Scenario { get; set; } = new ScenarioTable(); - public BannedPlayers BanList { get; set; } = new BannedPlayers(); + public BanListTable BanList { get; set; } = new BanListTable(); public DiscordTable Discord { get; set; } = new DiscordTable(); public ShineTable Shines { get; set; } = new ShineTable(); public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable(); @@ -58,15 +58,16 @@ public class ScenarioTable { public bool MergeEnabled { get; set; } = false; } - public class BannedPlayers { + public class BanListTable { public bool Enabled { get; set; } = false; - public List Players { get; set; } = new List(); - public List IpAddresses { get; set; } = new List(); + public ISet Players { get; set; } = new SortedSet(); + public ISet IpAddresses { get; set; } = new SortedSet(); + public ISet Stages { get; set; } = new SortedSet(); } public class FlipTable { public bool Enabled { get; set; } = true; - public List Players { get; set; } = new List(); + public ISet Players { get; set; } = new SortedSet(); public FlipOptions Pov { get; set; } = FlipOptions.Both; } @@ -79,6 +80,7 @@ public class DiscordTable { public class ShineTable { public bool Enabled { get; set; } = true; + public ISet Excluded { get; set; } = new SortedSet { 496 }; } public class PersistShinesTable @@ -86,4 +88,4 @@ public class PersistShinesTable public bool Enabled { get; set; } = false; public string Filename { get; set; } = "./moons.json"; } -} \ No newline at end of file +} diff --git a/Shared/Stages.cs b/Shared/Stages.cs index 5eb03b4..04315d5 100644 --- a/Shared/Stages.cs +++ b/Shared/Stages.cs @@ -11,7 +11,7 @@ public static class Stages { return mapName; } // exact stage value - if (Stage2Alias.ContainsKey(input)) { + if (IsStage(input)) { return input; } // force input value with a ! @@ -29,6 +29,32 @@ public static string KingdomAliasMapping() { return result; } + public static bool IsAlias(string input) { + return Alias2Stage.ContainsKey(input); + } + + public static bool IsStage(string input) { + return Stage2Alias.ContainsKey(input); + } + + public static IEnumerable StagesByInput(string input) { + if (IsAlias(input)) { + var stages = Stage2Alias + .Where(e => e.Value == input) + .Select(e => e.Key) + ; + foreach (string stage in stages) { + yield return stage; + } + } + else { + string? stage = Input2Stage(input); + if (stage != null) { + yield return stage; + } + } + } + public static readonly Dictionary Alias2Stage = new Dictionary() { { "cap", "CapWorldHomeStage" }, { "cascade", "WaterfallWorldHomeStage" }, diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..d1b9d07 --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +if [[ "$#" == "0" ]] || [[ "$#" > "1" ]] || ! [[ "$1" =~ ^(all|x64|arm|arm64|win64)$ ]] ; then + echo "Usage: docker-build.sh {all|x64|arm|arm64|win64}" + exit 1 +fi + +DIR=$(dirname "$(realpath $0)") +cd "$DIR" + +declare -A archs=( + ["x64"]="linux-x64" + ["arm"]="linux-arm" + ["arm64"]="linux-arm64" + ["win64"]="win-x64" +) + +for sub in "${!archs[@]}" ; do + arch="${archs[$sub]}" + + if [[ "$1" != "all" ]] && [[ "$1" != "$sub" ]] ; then + continue + fi + + docker run \ + -u `id -u`:`id -g` \ + -v "/$DIR/"://app/ \ + -w //app/ \ + -e DOTNET_CLI_HOME=//app/cache/ \ + -e XDG_DATA_HOME=//app/cache/ \ + mcr.microsoft.com/dotnet/sdk:6.0 \ + dotnet publish \ + ./Server/Server.csproj \ + -r $arch \ + -c Release \ + -o /app/bin/$sub/ \ + --self-contained \ + -p:publishSingleFile=true \ + ; + + filename="Server" + ext="" + if [[ "$sub" == "arm" ]] ; then filename="Server.arm"; + elif [[ "$sub" == "arm64" ]] ; then filename="Server.arm64"; + elif [[ "$sub" == "win64" ]] ; then filename="Server.exe"; ext=".exe"; + fi + + mv ./bin/$sub/Server$ext ./bin/$filename + rm -rf ./bin/$sub/ +done