diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 8f1b984e..00000000 --- a/.gitmodules +++ /dev/null @@ -1,5 +0,0 @@ -[submodule "CelesteNet.Extras"] - path = CelesteNet.Extras - url = https://github.com/CelestialNetworkingEngineers/CelesteNet.Extras - update = merge - branch = main diff --git a/CelesteNet.Client/Components/CelesteNetClientInfoComponent.cs b/CelesteNet.Client/Components/CelesteNetClientInfoComponent.cs new file mode 100644 index 00000000..c5a9f5be --- /dev/null +++ b/CelesteNet.Client/Components/CelesteNetClientInfoComponent.cs @@ -0,0 +1,338 @@ +/* FIXME: Warning: Only ever define this for Testing! Not for public builds! + Since I've been building Release builds to see full Confuser output, I can't switch this based on Debug builds + and we release the client itself as Debug anyways (or not?). + Conditional Attribute on DebugLog() should make it so that all calls get removed by compiler! +*/ +#define CLIENT_INFO_DEBUG_LOG + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Security.Cryptography; +using System.Text; +using Celeste.Mod.CelesteNet.DataTypes; +using Microsoft.Win32; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Celeste.Mod.CelesteNet.Client.Components { + public class CelesteNetClientInfoComponent : CelesteNetGameComponent { + + private readonly System.Collections.IDictionary ClientInfo; + private DataClientInfoRequest request; + + public CelesteNetClientInfoComponent(CelesteNetClientContext context, Game game) : base(context, game) { + + ClientInfo = Environment.GetEnvironmentVariables(); + + DebugLog(LogLevel.INF, "clientinfo", $"OSVersion {Environment.OSVersion.Platform} {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}"); + } + + [Conditional("CLIENT_INFO_DEBUG_LOG")] + private static void DebugLog(LogLevel lvl, string prefix, string msg) { + Logger.Log(lvl, prefix, msg); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string ClientInfoGet(string inp) { + string entry; + try { + entry = Encoding.UTF8.GetString(Convert.FromBase64String(inp)); + } catch (Exception e) { + DebugLog(LogLevel.INF, "clientinfo", $"Caught {e} trying to get {inp}"); + return ""; + } + DebugLog(LogLevel.INF, "clientinfo", $"Trying to get {entry} ({inp}) from {string.Join(",", ClientInfo.Keys)}"); + + try { + PropertyInfo prop = typeof(Environment).GetProperty(entry, BindingFlags.Static | BindingFlags.Public); + if (prop != null) + return prop.GetValue(typeof(Environment)).ToString(); + } catch (Exception e) { + DebugLog(LogLevel.INF, "clientinfo", $"Caught {e} trying to get {inp} via prop"); + return ""; + } + + if (ClientInfo.Contains(entry)) + return (string)ClientInfo[entry]; + return ""; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private NetworkInterface FindNicForAddress(IPAddress address) => NetworkInterface.GetAllNetworkInterfaces().FirstOrDefault(i => i.GetIPProperties().UnicastAddresses.Any(a => a.Address.Equals(address))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private NetworkInterface FindInternetFacingNic() { + IPAddress? addrInternet = null; + Socket socket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + try { + socket.Connect("8.8.8.8", 80); + addrInternet = ((IPEndPoint)socket.LocalEndPoint).Address; + } catch { } finally { + socket.Close(); + } + DebugLog(LogLevel.INF, "handle", $"addrInternet is {addrInternet}"); + + if (addrInternet?.AddressFamily == AddressFamily.InterNetwork || addrInternet?.AddressFamily == AddressFamily.InterNetworkV6) { + return FindNicForAddress(addrInternet); + } + + return null; + } + + [SupportedOSPlatform("windows")] + private string GetRegistryDev(string nicId) { + if (request == null || !request.IsValid) + return ""; + string fRegistryKey = request.MapStrings[0] + nicId + request.MapStrings[1]; + + try { + RegistryKey rk = Registry.LocalMachine.OpenSubKey(fRegistryKey, false); + return rk?.GetValue(request.MapStrings[2], "").ToString() ?? ""; + } catch { } + return ""; + } + + [SupportedOSPlatform("windows")] + private bool checkNicWindows(string dev) { + if (request == null || !request.IsValid) + return false; + string dId = GetRegistryDev(dev); + DebugLog(LogLevel.INF, "checkNicWindows", $"Checking {request.MapStrings[2]} {dId}"); + + return dId.Trim().ToLower().StartsWith("pci"); + } + + [SupportedOSPlatform("windows")] + private string GetGUIDWindows() { + if (request == null || !request.IsValid) + return ""; + try { + RegistryKey rk = Registry.LocalMachine.OpenSubKey(request.MapStrings[3], false); + return rk?.GetValue(request.MapStrings[4], "").ToString() ?? ""; + } catch { } + return ""; + } + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private bool checkNicLinux(string dev) { + string sysClassPath = $"/sys/class/net/{dev}/device/modalias"; + + if (!File.Exists(sysClassPath)) { + return false; + } + + try { + string text; + using (StreamReader streamReader = File.OpenText(sysClassPath)) { + text = streamReader.ReadToEnd(); + } + + DebugLog(LogLevel.INF, "checkNicLinux", $"Checking modalias {text}"); + + return text.Trim().ToLower().StartsWith("pci"); + } catch { } + return false; + } + + private NetworkInterface FindBestNicPerPlatform(bool excludeWireless = true) { + foreach (var n in NetworkInterface.GetAllNetworkInterfaces()) { + if (OperatingSystem.IsWindows()) { + if (checkNicWindows(n.Id) && (!excludeWireless || n.NetworkInterfaceType == NetworkInterfaceType.Ethernet)) { + DebugLog(LogLevel.INF, "FindBestNicPerPlatform", $"Returning nic {n.Id}"); + return n; + } + } else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { + if (checkNicLinux(n.Id) && (!excludeWireless || n.NetworkInterfaceType == NetworkInterfaceType.Ethernet)) { + DebugLog(LogLevel.INF, "FindBestNicPerPlatform", $"Returning nic {n.Id}"); + return n; + } + } + } + return null; + } + + [SupportedOSPlatform("macos")] + private string GetGUIDMac() { + if (request == null || !request.IsValid) + return ""; + + var startInfo = new ProcessStartInfo() { + FileName = "sh", + Arguments = $"-c \"{request.MapStrings[5]}\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + //RedirectStandardError = true, + //RedirectStandardInput = true, + //UserName = Environment.UserName + }; + var builder = new StringBuilder(); + using (Process process = Process.Start(startInfo)) { + process.WaitForExit(); + builder.Append(process.StandardOutput.ReadToEnd()); + } + string procOut = builder.ToString(); + + foreach (var line in procOut.ReplaceLineEndings("\n").Split('\n')) { + if (!line.Contains(request.MapStrings[6])) + continue; + + var kv = line.Split('='); + + if (kv.Length > 1) { + return kv[1].Trim().Trim('"'); + } + } + + return ""; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetAdapterInfo() { + string info = ""; + GraphicsAdapter adapter = GraphicsAdapter.DefaultAdapter; + // some of these tend to throw NotImplementedExceptions but we'll just move on... + try { + info += adapter.VendorId.ToString(); + } catch { } + try { + info += ":" + adapter.DeviceId.ToString(); + } catch { } + try { + info += " " + adapter.DeviceName; + } catch { } + try { + info += " " + adapter.Description; + } catch { } + return info; + } + + [SupportedOSPlatform("linux")] + private string GetGUIDLinux() { + if (request == null || !request.IsValid) + return ""; + + string[] paths = new[] { request.MapStrings[7], request.MapStrings[8] }; + + foreach (string path in paths) { + if (!File.Exists(path)) { + continue; + } + + try { + string text; + using (StreamReader streamReader = File.OpenText(path)) { + text = streamReader.ReadToEnd(); + } + + return text.Trim(); + } catch { + } + } + + return ""; + } + + private string GetPlatformDevId() { + if (OperatingSystem.IsWindows()) { + return GetGUIDWindows(); + } else if (OperatingSystem.IsLinux()) { + return GetGUIDLinux(); + } else if (OperatingSystem.IsMacOS()) { + return GetGUIDMac(); + } else { + return ""; + } + } + + public void Handle(CelesteNetConnection con, DataClientInfoRequest data) { + Logger.Log(LogLevel.INF, "handle", "Got DataClientInfoRequest"); + + request = data; + + DataClientInfo info = new() { + Nonce = data.Nonce + }; + + if (!data.IsValid) { + Logger.Log(LogLevel.WRN, "handle", $"DataClientInfoRequest invalid."); + con.Send(info); + return; + } + +#if CLIENT_INFO_DEBUG_LOG + foreach (var n in NetworkInterface.GetAllNetworkInterfaces()) { + DebugLog(LogLevel.INF, "handle", $"Nic {n} {n.GetPhysicalAddress()}"); + foreach (var ip in n.GetIPProperties().UnicastAddresses.Select(i => i.Address)) { + DebugLog(LogLevel.INF, "handle", $"{ip.AddressFamily} {ip}"); + } + + if (OperatingSystem.IsWindows()) { + string dId = GetRegistryDev(n.Id); + if (dId.Length > 3 && dId.Substring(0, 3) == "PCI") { + DebugLog(LogLevel.INF, "handle", $"{n.Name} is {dId}"); + } + } + if (OperatingSystem.IsMacOS()) { + DebugLog(LogLevel.INF, "handle", $"Mac GUID: {GetGUIDMac()}"); + } + } +#endif + + NetworkInterface nic = FindBestNicPerPlatform() ?? FindNicForAddress(((IPEndPoint)((CelesteNetTCPUDPConnection)con).TCPSocket.LocalEndPoint).Address); + PhysicalAddress mac = nic?.GetPhysicalAddress(); + + if (nic == null || mac.ToString().IsNullOrEmpty()) { + DebugLog(LogLevel.INF, "handle", $"No MAC found, trying internet socket..."); + nic = FindInternetFacingNic(); + mac = nic?.GetPhysicalAddress(); + } + + string platformDevId = GetPlatformDevId().Trim(); + + if (platformDevId.IsNullOrEmpty() || mac == null) { + Logger.Log(LogLevel.WRN, "handle", $"DataClientInfo could not gather proper response."); + con.Send(info); + return; + } + + string checkEnv = $"{Environment.OSVersion.Platform}-" + string.Join("-", data.List.Select(k => ClientInfoGet(k))).Trim(); + string checkHW = $"{GetAdapterInfo()}-{(int)Everest.SystemMemoryMB}".Trim(); + string checkId = platformDevId.IsNullOrEmpty() ? "" : $"id-{platformDevId}"; + + DebugLog(LogLevel.INF, "handle", $"DataClientInfo inputs: {checkEnv} {mac} {checkId}"); + + using (SHA256 sha256Hash = SHA256.Create()) { + info.ConnInfoA = Convert.ToBase64String(sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(checkId))); + info.ConnInfoB = mac != null ? Convert.ToBase64String(sha256Hash.ComputeHash(mac.GetAddressBytes())) : ""; + info.ConnInfoC = Convert.ToBase64String(sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(checkEnv))); + + // self-report + ConnectionErrorCodeException ceee = Context.Client?.LastConnectionError; + if (ceee?.StatusCode == 401) { + info.ConnInfoD = ceee.Status; + } + } + + DebugLog(LogLevel.INF, "handle", $"Sending DataClientInfo - {info.ConnInfoA} - {info.ConnInfoB} - {info.ConnInfoC} - {info.ConnInfoD}"); + + con.Send(info); + } + + /* + public override void Init() { + base.Init(); + DataHandler handler = (con, data) => handleReq(con, data); + Client?.Data?.RegisterHandler(handler); + }*/ + } +} diff --git a/CelesteNet.Server.FrontendModule/CelesteNet.Server.FrontendModule.csproj b/CelesteNet.Server.FrontendModule/CelesteNet.Server.FrontendModule.csproj index 1def2055..132f0d5b 100644 --- a/CelesteNet.Server.FrontendModule/CelesteNet.Server.FrontendModule.csproj +++ b/CelesteNet.Server.FrontendModule/CelesteNet.Server.FrontendModule.csproj @@ -11,10 +11,6 @@ - - - - diff --git a/CelesteNet.Server.FrontendModule/WSCMDs/WSCMDsExtra.cs b/CelesteNet.Server.FrontendModule/WSCMDs/WSCMDsExtra.cs new file mode 100644 index 00000000..9f83de77 --- /dev/null +++ b/CelesteNet.Server.FrontendModule/WSCMDs/WSCMDsExtra.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using Celeste.Mod.CelesteNet.DataTypes; +using Celeste.Mod.CelesteNet.Server.Chat; +using MonoMod.Utils; + +namespace Celeste.Mod.CelesteNet.Server.Control { + public class WSCMDBanExt : WSCMD { + public override bool MustAuth => true; + public override object? Run(dynamic? input) { + uint id = (uint)input?.ID; + string? connInfo = (string?)input?.ConnInfo; + bool? banConnUID = (bool?)input?.BanConnUID; + string? reason = (string?)input?.Reason; + string? connUid = (string?)input?.ConnUID; + bool quiet = ((bool?)input?.Quiet) ?? false; + + Logger.Log(LogLevel.VVV, "frontend", $"BanExt called:\n{connUid} => {connInfo} (ban connUID: {banConnUID}, q: {quiet})\nReason: {reason}"); + + if ((connInfo = connInfo?.Trim() ?? "").IsNullOrEmpty() || + (reason = reason?.Trim() ?? "").IsNullOrEmpty() || + (connUid = connUid?.Trim() ?? "").IsNullOrEmpty()) + return false; + + // connInfo should include "key" at this point e.g. CheckMAC#Y2F0IGdvZXMgbWVvdw== + string[] splitConnInfo = connInfo.Split(ConnFeatureUtils.kvSeparator); + Logger.Log(LogLevel.VVV, "frontend", $"BanExt split connInfo: {splitConnInfo}"); + + if (splitConnInfo.Length < 2) + return false; + + CelesteNetPlayerSession[] players; + CelesteNetPlayerSession? p; + + using (Frontend.Server.ConLock.R()) { + players = Frontend.Server.Sessions.ToArray(); + + Frontend.Server.PlayersByID.TryGetValue(id, out p); + } + + string connInfoVal = splitConnInfo[1]; + + // Just to make extra sure we got the right guy, I guess + if (p != null && p.Con is ConPlusTCPUDPConnection pCon + && pCon.ConnFeatureData.TryGetValue(splitConnInfo[0], out string? connVal) + && !string.IsNullOrEmpty(connVal)) { + connInfoVal = connVal; + } + + // UID will be the connectionUID as extra info, further down the banned identifier is where it'll be stored + BanInfo ban = new() { + UID = connUid, + Reason = reason, + From = DateTime.UtcNow + }; + + foreach (CelesteNetPlayerSession player in players) { + ConPlusTCPUDPConnection? plusCon = player.Con as ConPlusTCPUDPConnection; + if (plusCon == null) + continue; + + if (!plusCon.ConnFeatureData.ContainsValue(connInfoVal)) + continue; + + if (ban.Name.IsNullOrEmpty()) + ban.Name = player.PlayerInfo?.FullName ?? ""; + + ChatModule chat = Frontend.Server.Get(); + if (!quiet) + new DynamicData(player).Set("leaveReason", chat.Settings.MessageBan); + player.Con.Send(new DataDisconnectReason { Text = "Banned: " + reason }); + player.Con.Send(new DataInternalDisconnect()); + player.Dispose(); + } + + // stored with the "full" connInfo which includes the "key" e.g. CheckMAC#Y2F0IGdvZXMgbWVvdw== + Frontend.Server.UserData.Save(connInfo, ban); + + Logger.Log(LogLevel.VVV, "frontend", $"BanExt ban saved for: {connInfo}"); + + if (banConnUID ?? false) { + // reuse banInfo but store for connection UID like a regular ban + Frontend.Server.UserData.Save(connUid, ban); + Logger.Log(LogLevel.VVV, "frontend", $"BanExt ban saved for: {connUid}"); + } + + Frontend.BroadcastCMD(true, "update", Frontend.Settings.APIPrefix + "/userinfos"); + + return true; + } + } +} diff --git a/CelesteNet.Server/CelesteNet.Server.csproj b/CelesteNet.Server/CelesteNet.Server.csproj index 82fa9b76..4329ea11 100644 --- a/CelesteNet.Server/CelesteNet.Server.csproj +++ b/CelesteNet.Server/CelesteNet.Server.csproj @@ -10,13 +10,6 @@ - - - - - - - diff --git a/CelesteNet.Server/ConPlus/ConnFeatureUtils.cs b/CelesteNet.Server/ConPlus/ConnFeatureUtils.cs new file mode 100644 index 00000000..e23a61a4 --- /dev/null +++ b/CelesteNet.Server/ConPlus/ConnFeatureUtils.cs @@ -0,0 +1,61 @@ +using System; + +namespace Celeste.Mod.CelesteNet.Server { + public static class ConnFeatureUtils { + + public const char kvSeparator = '#'; + public const string CHECK_ENTRY_ENV = "CheckEnv"; + public const string CHECK_ENTRY_MAC = "CheckMAC"; + public const string CHECK_ENTRY_DEV = "CheckDevice"; + public const string CHECK_ENTRY_BAN = "SelfReportBan"; + + public static string? ClientCheck(ConPlusTCPUDPConnection Con) { + string? selfReport; + string? CheckDevice, CheckDeviceKV; + string? CheckMAC, CheckMacKV; + string? CheckEnv, CheckEnvKV; + + if (Con.ConnFeatureData.TryGetValue(CHECK_ENTRY_BAN, out selfReport) && !selfReport.Trim().IsNullOrEmpty()) { + BanInfo banMe = new() { + UID = Con.UID, + // I had this prefix it with "Auto-ban" but since we don't have separate "reasons" for internal + // documentation vs. what is shown to the client, I'd like to hide the fact that this is an extra + // "automated" ban happening. + Reason = "-> " + selfReport, + From = DateTime.UtcNow + }; + Con.Server.UserData.Save(Con.UID, banMe); + + Logger.Log(LogLevel.VVV, "frontend", $"Auto-ban of secondary IP: {selfReport} ({Con.UID})"); + + return Con.Server.Settings.MessageClientCheckFailed; + } + + if (!Con.ConnFeatureData.TryGetValue(CHECK_ENTRY_DEV, out CheckDevice) || CheckDevice.Trim().IsNullOrEmpty()) + return Con.Server.Settings.MessageClientCheckFailed; + CheckDeviceKV = CHECK_ENTRY_DEV + kvSeparator + CheckDevice; + + if (!Con.ConnFeatureData.TryGetValue(CHECK_ENTRY_MAC, out CheckMAC) || CheckMAC.Trim().IsNullOrEmpty()) + return Con.Server.Settings.MessageClientCheckFailed; + CheckMacKV = CHECK_ENTRY_MAC + kvSeparator + CheckMAC; + + if (!Con.ConnFeatureData.TryGetValue(CHECK_ENTRY_ENV, out CheckEnv) || CheckEnv.Trim().IsNullOrEmpty()) + return Con.Server.Settings.MessageClientCheckFailed; + CheckEnvKV = CHECK_ENTRY_ENV + kvSeparator + CheckEnv; + + // Check if the player's banned + BanInfo ban; + + // relying on short-circuiting of || here... + bool found = Con.Server.UserData.TryLoad(CheckMacKV, out ban) + || Con.Server.UserData.TryLoad(CheckDeviceKV, out ban) + || Con.Server.UserData.TryLoad(CheckEnvKV, out ban); + + if (found && (ban.From == null || ban.From <= DateTime.Now) && (ban.To == null || DateTime.Now <= ban.To)) { + return string.Format(Con.Server.Settings.MessageBan, "", "", ban.Reason); + } + + return null; + } + } +} \ No newline at end of file diff --git a/CelesteNet.Server/ConPlus/HandshakeFeature.cs b/CelesteNet.Server/ConPlus/HandshakeFeature.cs new file mode 100644 index 00000000..27af6168 --- /dev/null +++ b/CelesteNet.Server/ConPlus/HandshakeFeature.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Celeste.Mod.CelesteNet.DataTypes; +using Celeste.Mod.CelesteNet.Server; + +namespace Celeste.Mod.CelesteNet { + public class ExtendedHandshake : IConnectionFeature { + + public async Task DoHandshake(CelesteNetConnection con, bool isClient) { + if (isClient) + throw new InvalidOperationException($"ExtendedHandshake was called with 'isClient == true' in Server context!"); + + ConPlusTCPUDPConnection? Con = (ConPlusTCPUDPConnection?)con; + + if (Con == null) { + Logger.Log(LogLevel.VVV, "handshakeFeature", $"Con was null"); + return; + } + + if (!Con.Server.Settings.ClientChecks) { + return; + } + + CancellationTokenSource cts = new CancellationTokenSource(); + + string Nonce = Con.ConnectionToken.ToString(); + + Con.Server.Data.WaitFor(400, (con, data) => { + if (Con != con) + return false; + + if (data.IsValid) { + if (data.ConnInfoD.Trim().Length > 0) { + // self-report + Con.ConnFeatureData.Add(ConnFeatureUtils.CHECK_ENTRY_BAN, data.ConnInfoD); + + } else { + Con.ConnFeatureData.Add(ConnFeatureUtils.CHECK_ENTRY_DEV, data.ConnInfoA); + Con.ConnFeatureData.Add(ConnFeatureUtils.CHECK_ENTRY_MAC, data.ConnInfoB); + Con.ConnFeatureData.Add(ConnFeatureUtils.CHECK_ENTRY_ENV, data.ConnInfoC); + } + } + + Logger.Log(LogLevel.VVV, "handshakeFeature", $"Got DataClientInfo for {Con?.UID} = {data.ConnInfoA} - {data.ConnInfoB} - {data.ConnInfoC} - {data.ConnInfoD}"); + cts.Cancel(); + return true; + }, () => { + Logger.Log(LogLevel.VVV, "handshakeFeature", $"Awaiting DataClientInfo timed out for {Con?.UID}"); + cts.Cancel(); + }); + + Logger.Log(LogLevel.VVV, "handshakeFeature", $"Sending DataClientInfoRequest to {Con?.UID}"); + Con?.Send(new DataClientInfoRequest { + Nonce = Nonce, + List = new string[] { + "UHJvY2Vzc29yQ291bnQ=", + "TWFjaGluZU5hbWU=" + }, + MapStrings = new string[] { + "U1lTVEVNXFxDdXJyZW50Q29udHJvbFNldFxcQ29udHJvbFxcTmV0d29ya1xcezREMzZFOTcyLUUzMjUtMTFDRS1CRkMxLTA4MDAyQkUxMDMxOH1cXA==", "XFxDb25uZWN0aW9u", + "UG5wSW5zdGFuY2VJRA==", "U09GVFdBUkVcXE1pY3Jvc29mdFxcQ3J5cHRvZ3JhcGh5", "TWFjaGluZUd1aWQ=", "aW9yZWcgLXJkMSAtYyBJT1BsYXRmb3JtRXhwZXJ0RGV2aWNl", + "SU9QbGF0Zm9ybVVVSUQ=", "L3Zhci9saWIvZGJ1cy9tYWNoaW5lLWlk", "L2V0Yy9tYWNoaW5lLWlk", + } + }); + + // if we need hold back on establishing a session + if (!cts.Token.IsCancellationRequested) { + try { + await Task.Delay(400, cts.Token); + } catch (TaskCanceledException ex) { + Logger.Log(LogLevel.VVV, "handshakeFeature", $"Delay skipped by TaskCanceledException: {ex.Message}"); + } + } + } + + public void Register(CelesteNetConnection con, bool isClient) { + if (isClient) + throw new InvalidOperationException($"ExtendedHandshake was called with 'isClient == true' in Server context!"); + + // Got nothing to do here + } + } +} \ No newline at end of file diff --git a/CelesteNet.Shared/CelesteNet.Shared.csproj b/CelesteNet.Shared/CelesteNet.Shared.csproj index 2a737ce4..487dd2ab 100644 --- a/CelesteNet.Shared/CelesteNet.Shared.csproj +++ b/CelesteNet.Shared/CelesteNet.Shared.csproj @@ -8,7 +8,6 @@ - diff --git a/CelesteNet.Shared/ConnectionFeatures/ConnFeatureUtils.cs b/CelesteNet.Shared/ConnectionFeatures/ConnFeatureUtils.cs deleted file mode 100644 index 44b45e39..00000000 --- a/CelesteNet.Shared/ConnectionFeatures/ConnFeatureUtils.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Celeste.Mod.CelesteNet.Server { - public static class ConnFeatureUtils { - - /* stubbed for optional implementation */ - public static string? ClientCheck(ConPlusTCPUDPConnection Con) { - return null; - } - } -} \ No newline at end of file diff --git a/CelesteNet.Shared/DataTypes/DataClientInfo.cs b/CelesteNet.Shared/DataTypes/DataClientInfo.cs new file mode 100644 index 00000000..a2544495 --- /dev/null +++ b/CelesteNet.Shared/DataTypes/DataClientInfo.cs @@ -0,0 +1,151 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Celeste.Mod.CelesteNet.DataTypes { + public class DataClientInfo : DataType, IDataRequestable { + + static DataClientInfo() { + DataID = "clientInfo"; + } + + public override DataFlags DataFlags => DataFlags.Taskable; + + public uint RequestID = uint.MaxValue; + + public string Nonce = ""; + + // These could be "anything" in the client, so A, B and C they shall be... + public string ConnInfoA = ""; + public string ConnInfoB = ""; + public string ConnInfoC = ""; + public string ConnInfoD = ""; + + private bool isValidated = false; + public bool IsValid => isValidated && ConnInfoA.Length > 0 && ConnInfoB.Length > 0 && ConnInfoC.Length > 0; + + public override MetaType[] GenerateMeta(DataContext ctx) + => new MetaType[] { + new MetaRequestResponse(RequestID) + }; + + public override void FixupMeta(DataContext ctx) { + RequestID = Get(ctx); + } + + /* Here in Read and Write, the strings are ran through "WireData()" + * before writing to/reading from the "wire", so that the strings look + * less obvious in the data packets. + * It's just some XOR shenanigans. + */ + + protected override void Read(CelesteNetBinaryReader reader) { + Nonce = reader.ReadNetString(); + ConnInfoA = WireData(reader.ReadNetString(), Nonce); + ConnInfoB = WireData(reader.ReadNetString(), Nonce); + ConnInfoC = WireData(reader.ReadNetString(), Nonce); + ConnInfoD = WireData(reader.ReadNetString(), Nonce); + if (ConnInfoA.StartsWith(Nonce) + && ConnInfoB.StartsWith(Nonce) + && ConnInfoC.StartsWith(Nonce) + && ConnInfoD.StartsWith(Nonce)) { + ConnInfoA = ConnInfoA.Substring(Nonce.Length); + ConnInfoB = ConnInfoB.Substring(Nonce.Length); + ConnInfoC = ConnInfoC.Substring(Nonce.Length); + ConnInfoD = ConnInfoD.Substring(Nonce.Length); + isValidated = true; + } else { + isValidated = false; + } + } + + protected override void Write(CelesteNetBinaryWriter writer) { + writer.WriteNetString(Nonce); + writer.WriteNetString(WireData(Nonce + ConnInfoA, Nonce)); + writer.WriteNetString(WireData(Nonce + ConnInfoB, Nonce)); + writer.WriteNetString(WireData(Nonce + ConnInfoC, Nonce)); + writer.WriteNetString(WireData(Nonce + ConnInfoD, Nonce)); + } + + public static string WireData(string inp, string mask) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < inp.Length; i++) + sb.Append((char)(((inp[i] - 'a') ^ mask[(i % mask.Length)]) + 'a')); + + return sb.ToString(); + } + } + + public class DataClientInfoRequest : DataType { + + static DataClientInfoRequest() { + DataID = "clientInfoReq"; + } + + public uint ID = uint.MaxValue; + + public string Nonce; + + public string[] List = Dummy.EmptyArray; + public string[] MapStrings = Dummy.EmptyArray; + + public bool IsValid => MapStrings.Length == checksums.Length; + + public override MetaType[] GenerateMeta(DataContext ctx) + => new MetaType[] { + new MetaRequest(ID) + }; + + public override void FixupMeta(DataContext ctx) { + ID = Get(ctx); + } + + private readonly string[] checksums = new string[] { + "360ac5c220a3033589229abfa126728e75a7318e9c05ac5b69021c55abf04398", + "9bb95f1941e2c94dd99c073e2f3d49ea15f84e353b63752eecdd9b70f2087078", + "704c3c7955b5df5e82a3217d5653019946077f9ba0a5bd63df30b848c6bea002", + "1e7ec6443675384242e877f42bcf45ca8a45632c533f9a18a842d825586c6037", + "36a79172dd1fdcd4e5a1409340a014060c834d5fdfd8d4d55a089f9c87462633", + "c1a5151fb910ba8074479d876d3a04d9588b44e97f9462e34b1f9c484cd9eafe", + "6402a261294e6f3180017bd427066422f691b66d58708f06d532dd9c4801a31e", + "e20381a73e871b0b732d4cee2cc10eab1a51d21983d417939efde6a399189684", + "2745c6c35be46aa1e23f694259acaf536247dc127bf7f728035891cdc1390992" + }; + + protected override void Read(CelesteNetBinaryReader reader) { + Nonce = reader.ReadNetString(); + List = new string[reader.ReadUInt16()]; + for (int i = 0; i < List.Length; i++) + List[i] = DataClientInfo.WireData(reader.ReadNetString(), Nonce); + MapStrings = new string[reader.ReadUInt16()]; + bool all_good = MapStrings.Length == checksums.Length; + using (SHA256 sha256Hash = SHA256.Create()) { + for (int i = 0; i < MapStrings.Length; i++) { + MapStrings[i] = DataClientInfo.WireData(reader.ReadNetString(), Nonce); + string hash = string.Concat(sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(MapStrings[i])).Select(b => string.Format("{0:x2}", b))); + Logger.Log(LogLevel.VVV, "dataclientinforequest", $"{MapStrings[i]} = {hash}"); + MapStrings[i] = Encoding.UTF8.GetString(Convert.FromBase64String(MapStrings[i])); + Logger.Log(LogLevel.VVV, "dataclientinforequest", $"= {MapStrings[i]}"); + if (all_good && hash != checksums[i]) + all_good = false; + } + } + if (!all_good) + MapStrings = Dummy.EmptyArray; + } + + protected override void Write(CelesteNetBinaryWriter writer) { + writer.WriteNetString(Nonce); + writer.Write((ushort)List.Length); + foreach (string req in List) { + writer.WriteNetString(DataClientInfo.WireData(req, Nonce)); + } + writer.Write((ushort)MapStrings.Length); + foreach (string req in MapStrings) { + writer.WriteNetString(DataClientInfo.WireData(req, Nonce)); + } + } + + } +} \ No newline at end of file diff --git a/CelesteNet.sln b/CelesteNet.sln index 7dd236ea..a6682d19 100644 --- a/CelesteNet.sln +++ b/CelesteNet.sln @@ -66,8 +66,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "emoji", "emoji", "{273EB989 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CelesteNet.Server.SqliteModule", "CelesteNet.Server.SqliteModule\CelesteNet.Server.SqliteModule.csproj", "{A43B6AC6-3305-4DCC-B8AE-A3FD7ACB3547}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CelesteNet.Extras", "CelesteNet.Extras\CelesteNet.Extras.csproj", "{7D416F3C-F603-4C58-BEDA-CAAB49A661EB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,10 +98,6 @@ Global {A43B6AC6-3305-4DCC-B8AE-A3FD7ACB3547}.Debug|Any CPU.Build.0 = Debug|Any CPU {A43B6AC6-3305-4DCC-B8AE-A3FD7ACB3547}.Release|Any CPU.ActiveCfg = Release|Any CPU {A43B6AC6-3305-4DCC-B8AE-A3FD7ACB3547}.Release|Any CPU.Build.0 = Release|Any CPU - {7D416F3C-F603-4C58-BEDA-CAAB49A661EB}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {7D416F3C-F603-4C58-BEDA-CAAB49A661EB}.Debug|Any CPU.Build.0 = Release|Any CPU - {7D416F3C-F603-4C58-BEDA-CAAB49A661EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D416F3C-F603-4C58-BEDA-CAAB49A661EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PublishClient.sh b/PublishClient.sh index bfc6276e..0da08e1f 100755 --- a/PublishClient.sh +++ b/PublishClient.sh @@ -5,9 +5,6 @@ mkdir PubClient cp everest.pubclient.yaml PubClient/everest.yaml cp -r CelesteNet.Client/bin/Release/net7.0/CelesteNet.* PubClient [ -f PubClient/CelesteNet.Client.deps.json ] && rm PubClient/CelesteNet.Client.deps.json -if [ -f CelesteNet.Extras/bin/Release/net7.0/CelesteNet.Extras.dll ]; then - cp CelesteNet.Extras/bin/Release/net7.0/CelesteNet.Extras.dll PubClient -fi cp -r Dialog PubClient cp -r Graphics PubClient cd PubClient; zip -r ../CelesteNet.Client.zip * diff --git a/everest.pubclient.yaml b/everest.pubclient.yaml index ef2b8968..a6297ed0 100644 --- a/everest.pubclient.yaml +++ b/everest.pubclient.yaml @@ -3,12 +3,4 @@ DLL: CelesteNet.Client.dll Dependencies: - Name: EverestCore - Version: 1.4465.0 -- Name: CelesteNet.Extras - Version: 2.2.2 - DLL: CelesteNet.Extras.dll - Dependencies: - - Name: EverestCore - Version: 1.4465.0 - - Name: CelesteNet.Client - Version: 2.2.2 \ No newline at end of file + Version: 1.4465.0 \ No newline at end of file diff --git a/everest.yaml b/everest.yaml index e8bd993c..2bf0eec0 100644 --- a/everest.yaml +++ b/everest.yaml @@ -4,11 +4,3 @@ Dependencies: - Name: EverestCore Version: 1.4465.0 -- Name: CelesteNet.Extras - Version: 2.2.2 - DLL: CelesteNet.Extras/bin/Release/net7.0/CelesteNet.Extras.dll - Dependencies: - - Name: EverestCore - Version: 1.4465.0 - - Name: CelesteNet.Client - Version: 2.2.2