From fc25760c2f7c9a2955d07cca268a3a0ffdc05af1 Mon Sep 17 00:00:00 2001 From: Thomas Woischnig Date: Mon, 4 Dec 2023 01:05:29 +0100 Subject: [PATCH] Direct connection support --- .../Runtime/LobbyClient.dll | Bin 27136 -> 34304 bytes .../Runtime/LobbyServerDto.dll | Bin 23040 -> 24576 bytes LobbyClient/LobbyClient.cs | 61 +++++- LobbyClient/UdpEchoServer.cs | 197 ++++++++++++++++++ LobbyClientTest/Program.cs | 26 ++- LobbyServer/Lobby.cs | 4 +- LobbyServer/Program.cs | 25 ++- LobbyServerDto/LobbyCreate.cs | 9 +- LobbyServerDto/LobbyHostInfo.cs | 9 +- LobbyServerDto/LobbyUpdate.cs | 13 +- .../LobbyMessageSourceGenerator.cs | 89 +++++++- .../LobbyServerSourceGenerator.csproj | 4 - 12 files changed, 397 insertions(+), 40 deletions(-) create mode 100644 LobbyClient/UdpEchoServer.cs diff --git a/Assets/NetworkLobbyClient/Runtime/LobbyClient.dll b/Assets/NetworkLobbyClient/Runtime/LobbyClient.dll index b750ab335016a98502b0f22b36b790b9f341ca95..6a05c08da8e68e8202da8d7d5d358ed7e45944ae 100644 GIT binary patch literal 34304 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 JtPk|S{{ul5jW_@R literal 27136 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 literal 23040 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 VjHtgE`-5LN!+*j2{D0Kk{{_F}=l=iz diff --git a/LobbyClient/LobbyClient.cs b/LobbyClient/LobbyClient.cs index 3554cb2..b6c42dc 100644 --- a/LobbyClient/LobbyClient.cs +++ b/LobbyClient/LobbyClient.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -18,6 +19,8 @@ namespace Lobbies AutoResetEvent waitForExternalIp = new AutoResetEvent(false); + UdpEchoServer udpEchoServer = new UdpEchoServer(); + private Dictionary lobbyInformation = new Dictionary(); private string? host; private int port; @@ -75,6 +78,8 @@ namespace Lobbies public void HostLobby(Guid gameId, string name, int gameMode, int maxPlayerCount, string? password, string? ip, int port) { + udpEchoServer.Start(0); + byte[]? hash = null, salt = null; if(!string.IsNullOrEmpty(password)) @@ -91,8 +96,9 @@ namespace Lobbies PlayerCount = 0, PasswordHash = hash, PasswordSalt = salt, - HostIp = ip, - HostPort = port + HostIps = GatherLocalIpAddresses().ToArray(), + HostPort = port, + HostTryPort = udpEchoServer.Port }; byte[] messageData = bufferRental.Rent(); @@ -142,7 +148,7 @@ namespace Lobbies }); } - public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, string? ip, int port) + public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, int port) { byte[]? hash = null, salt = null; @@ -159,8 +165,9 @@ namespace Lobbies PlayerCount = playerCount, PasswordHash = hash, PasswordSalt = salt, - HostIp = ip, - HostPort = port + HostIps = GatherLocalIpAddresses().ToArray(), + HostPort = port, + HostTryPort = udpEchoServer.Port }; byte[] messageData = bufferRental.Rent(); @@ -170,6 +177,8 @@ namespace Lobbies public void CloseLobby() { + udpEchoServer.Stop(); + var lobbyDelete = new LobbyDelete() { @@ -207,6 +216,32 @@ namespace Lobbies _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } + public async Task TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort) + { + return await Task.Run(() => + { + IPAddress? ret = null; + using (var udpEchoClient = new UdpEchoServer()) + { + udpEchoClient.Reached += (ep) => + { + ret = ep.Address; + }; + + udpEchoClient.Start(0); + + foreach (var ip in ipAddressesToTry) + { + udpEchoClient.CheckConnectionPossible(new IPEndPoint(ip, tryPort)); + } + + Thread.Sleep(500); + } + + return ret; + }); + } + public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted) { // Check if the hostname is already an IPAddress @@ -214,7 +249,7 @@ namespace Lobbies if (IPAddress.TryParse(hostName, out outIpAddress) == true) return new IPAddress[] { outIpAddress }; //<---------- - + IPAddress[] addresslist = Dns.GetHostAddresses(hostName); if (addresslist == null || addresslist.Length == 0) @@ -361,10 +396,24 @@ namespace Lobbies catch { } } + public IEnumerable GatherLocalIpAddresses() + { + foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + + foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses) + { + yield return addr.Address; + } + } + } + public void Dispose() { waitForExternalIp.Dispose(); tcpClient.Dispose(); + udpEchoServer.Dispose(); } } diff --git a/LobbyClient/UdpEchoServer.cs b/LobbyClient/UdpEchoServer.cs new file mode 100644 index 0000000..db63b30 --- /dev/null +++ b/LobbyClient/UdpEchoServer.cs @@ -0,0 +1,197 @@ +using System.Net.Sockets; +using System.Net; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace Lobbies +{ + /// + /// Small udp server to receive udp packets and echo the data back to source + /// + internal class UdpEchoServer : IDisposable + { + public const int SIO_UDP_CONNRESET = -1744830452; + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private bool running = false; + private bool isDisposed = false; + private UdpClient? serverSocketV4, serverSocketV6; + + public delegate void ReachableEventArgs(IPEndPoint remoteEndpoint); + /// + /// If a valid request for a ip and port query comes in call this event with the seen remote ip and port for a lobby server client id + /// + public event ReachableEventArgs? Reached; + + public int Port { get; private set; } + private bool isRunningV4 = false, isRunningV6 = false; + + public void CheckConnectionPossible(IPEndPoint remoteEndpoint) + { + if (!running || serverSocketV4 == null || serverSocketV6 == null) + throw new Exception("Listener not running!"); + + byte[] magicRequest = new byte[] { 0 }; + for (int i = 0; i < 16; i++) + if(remoteEndpoint.AddressFamily == AddressFamily.InterNetwork) + serverSocketV4.Send(magicRequest, magicRequest.Length, remoteEndpoint); + else + serverSocketV6.Send(magicRequest, magicRequest.Length, remoteEndpoint); + } + + /// + /// Listen to requests and fire events + /// + /// The port to listen on + private async void ListenV4(int port) + { + try + { + cancellationTokenSource.Token.ThrowIfCancellationRequested(); + serverSocketV4 = new UdpClient(port, AddressFamily.InterNetwork); + serverSocketV4.Client.IOControl( + (IOControlCode)SIO_UDP_CONNRESET, + new byte[] { 0, 0, 0, 0 }, + null + ); + Port = ((IPEndPoint)serverSocketV4.Client.LocalEndPoint).Port; + byte[] magicAnswer = new byte[] { 1 }; + using (cancellationTokenSource.Token.Register(() => { serverSocketV4.Close(); })) + { + isRunningV4 = true; + while (running) + { + var receiveResult = await serverSocketV4.ReceiveAsync(); + if (receiveResult.Buffer.Length == 1) + { + if (receiveResult.Buffer[0] == 0) + { + for (int i = 0; i < 16; i++) + serverSocketV4.Send(magicAnswer, magicAnswer.Length, receiveResult.RemoteEndPoint); + } + + if (receiveResult.Buffer[0] == 1) + { + Reached?.Invoke(receiveResult.RemoteEndPoint); + } + } + } + } + } + catch when (cancellationTokenSource.IsCancellationRequested || !running) //Cancel requested + { + + } + catch + { + throw; + } + finally + { + serverSocketV4?.Dispose(); + serverSocketV4 = null; + running = false; + isRunningV4 = false; + } + } + + /// + /// Listen to requests and fire events + /// + /// The port to listen on + private async void ListenV6(int port) + { + try + { + cancellationTokenSource.Token.ThrowIfCancellationRequested(); + serverSocketV6 = new UdpClient(port, AddressFamily.InterNetworkV6); + serverSocketV6.Client.IOControl( + (IOControlCode)SIO_UDP_CONNRESET, + new byte[] { 0, 0, 0, 0 }, + null + ); + byte[] magicAnswer = new byte[] { 1 }; + using (cancellationTokenSource.Token.Register(() => { serverSocketV6.Close(); })) + { + isRunningV6 = true; + while (running) + { + var receiveResult = await serverSocketV6.ReceiveAsync(); + if (receiveResult.Buffer.Length == 1) + { + if (receiveResult.Buffer[0] == 0) + { + Reached?.Invoke(receiveResult.RemoteEndPoint); + } + + if (receiveResult.Buffer[0] == 1) + { + for (int i = 0; i < 16; i++) + serverSocketV6.Send(magicAnswer, magicAnswer.Length, receiveResult.RemoteEndPoint); + } + } + } + } + } + catch when (cancellationTokenSource.IsCancellationRequested || !running) //Cancel requested + { + + } + catch + { + throw; + } + finally + { + serverSocketV6?.Dispose(); + serverSocketV6 = null; + running = false; + isRunningV6 = false; + } + } + + /// + /// Start udp listener + /// + /// The port to listen on + public void Start(int port) + { + isRunningV4 = false; + isRunningV6 = false; + running = true; + _ = Task.Run(() => ListenV4(port)); + while (running && !isRunningV4) + Thread.Yield(); + _ = Task.Run(() => ListenV6(Port)); + while (running && (!isRunningV4 || !isRunningV6)) + Thread.Yield(); + } + + /// + /// Stop udp listener + /// + public void Stop() + { + running = false; + cancellationTokenSource.Cancel(); + if (serverSocketV4 != null) + serverSocketV4?.Close(); + if (serverSocketV6 != null) + serverSocketV6?.Close(); + } + + public void Dispose() + { + if (!isDisposed) + { + Stop(); + while (isRunningV4 || isRunningV6) + Task.Yield(); + + cancellationTokenSource.Dispose(); + isDisposed = true; + } + } + } +} diff --git a/LobbyClientTest/Program.cs b/LobbyClientTest/Program.cs index 4d22e13..430252c 100644 --- a/LobbyClientTest/Program.cs +++ b/LobbyClientTest/Program.cs @@ -3,6 +3,7 @@ using Lobbies; using LobbyClientTest; using LobbyServerDto; using System.Net; +using System.Net.WebSockets; Console.WriteLine("Starting lobby client v0.7!"); var lobbyClient = new LobbyClient(); @@ -16,12 +17,11 @@ FakeGameHost fakeGameHost = new FakeGameHost(); int myPort = fakeGameHost.Server(0); string? myExternalIp = null; int myExternalPort = -1; -IPEndPoint? hostInfo = null; bool running = true; bool connected = false; -_ = Task.Run(() => +_ = Task.Run(async () => { while (running) { @@ -78,8 +78,23 @@ _ = Task.Run(() => var lobbyHostInfo = lobbyEvent.EventData as LobbyHostInfo; var p = Console.GetCursorPosition(); Console.SetCursorPosition(0, p.Top); - Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {lobbyHostInfo.HostIp}:{lobbyHostInfo.HostPort}!"); - hostInfo = new IPEndPoint(IPAddress.Parse(lobbyHostInfo.HostIp!), lobbyHostInfo.HostPort); + Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {(lobbyHostInfo.HostIps != null && lobbyHostInfo.HostIps.Length > 0 ? lobbyHostInfo.HostIps[0].ToString() : "")}:{lobbyHostInfo.HostPort}!"); + + //Try direct connection + if (lobbyHostInfo.HostIps != null && lobbyHostInfo.HostIps.Length > 0) + { + Console.WriteLine($"Trying direct connection to {string.Join(",", lobbyHostInfo.HostIps)} on port {lobbyHostInfo.HostTryPort}!"); + var reachableIp = await lobbyClient.TryDirectConnection(lobbyHostInfo.HostIps, lobbyHostInfo.HostTryPort); + if(reachableIp != null) + { + Console.WriteLine($"Direct connection to {reachableIp.ToString()} possible, using direct connection!"); + Console.WriteLine($"Connecting game client!"); + fakeGameHost.Send(new IPEndPoint(reachableIp, lobbyHostInfo.HostPort), "Hello from Game Client!"); + Console.Write(">"); + continue; + } + } + Console.WriteLine($"Requesting nat punch to me!"); lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, (remoteEndpoint, messageBuffer, messageLength) => { fakeGameHost.Send(remoteEndpoint, messageBuffer, messageLength); @@ -111,7 +126,7 @@ _ = Task.Run(() => Console.SetCursorPosition(0, p.Top); Console.WriteLine($"Nat punch requested to {lobbyRequestNatPunch!.ClientIp}:{lobbyRequestNatPunch.ClientPort}!"); - Task.Run(() => + _ = Task.Run(() => { lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) => { fakeGameHost.Send(remoteEndpoint, messageData, messageLength); @@ -140,7 +155,6 @@ _ = Task.Run(() => Console.SetCursorPosition(0, p.Top); Console.WriteLine($"Nat punch request done!"); Console.WriteLine($"Connecting game client!"); - fakeGameHost.Send(new IPEndPoint(IPAddress.Parse(lobbyNatPunchDone!.ExternalIp!), lobbyNatPunchDone.ExternalPort), "Hello from Game Client!"); Console.Write(">"); } diff --git a/LobbyServer/Lobby.cs b/LobbyServer/Lobby.cs index 5ba8b79..95eb0a2 100644 --- a/LobbyServer/Lobby.cs +++ b/LobbyServer/Lobby.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; @@ -24,7 +25,8 @@ namespace LobbyServer public int MaxPlayerCount { get; set; } public byte[]? PasswordHash { get; set; } public byte[]? PasswordSalt { get; set; } - public required string HostIp { get; set; } + public required IPAddress[] HostIps { get; set; } public int HostPort { get; set; } + public int HostTryPort { get; set; } } } diff --git a/LobbyServer/Program.cs b/LobbyServer/Program.cs index 7473bf0..9aa1bb6 100644 --- a/LobbyServer/Program.cs +++ b/LobbyServer/Program.cs @@ -1,7 +1,7 @@ using LobbyServer; using LobbyServerDto; using System.Collections.Concurrent; - +using System.Net; using var closing = new AutoResetEvent(false); using var tcpServer = new TcpServer(); @@ -42,6 +42,11 @@ tcpServer.DataReceived += (clientId, dataLength, data) => if (!GameGuids.ValidGuids.Contains(lobbyCreate.GameId)) throw new Exception("Invalid game guid!"); + List hostIpAddresses = new List(); + hostIpAddresses.Add(IPAddress.Parse(tcpServer.GetClientIp(clientId)!)); + if(lobbyCreate.HostIps != null) + hostIpAddresses.AddRange(lobbyCreate.HostIps); + var lobby = new Lobby { Name = lobbyCreate.Name, @@ -52,8 +57,9 @@ tcpServer.DataReceived += (clientId, dataLength, data) => PasswordHash = lobbyCreate.PasswordHash, PasswordSalt = lobbyCreate.PasswordSalt, HostClientId = clientId, - HostIp = lobbyCreate.HostIp == null ? tcpServer.GetClientIp(clientId)! : lobbyCreate.HostIp, + HostIps = hostIpAddresses.ToArray(), HostPort = lobbyCreate.HostPort, + HostTryPort = lobbyCreate.HostTryPort }; if(lobbiesByClientId.TryGetValue(clientId, out var existingLobby)) @@ -113,10 +119,17 @@ tcpServer.DataReceived += (clientId, dataLength, data) => existingLobby.PasswordHash = lobbyUpdate.PasswordHash; existingLobby.PasswordSalt = lobbyUpdate.PasswordSalt; - if (lobbyUpdate.HostIp != null) - existingLobby.HostIp = lobbyUpdate.HostIp; + List hostIpAddresses = new List(); + hostIpAddresses.Add(IPAddress.Parse(tcpServer.GetClientIp(clientId)!)); + if (lobbyUpdate.HostIps != null) + hostIpAddresses.AddRange(lobbyUpdate.HostIps); + + if (!Enumerable.SequenceEqual(existingLobby.HostIps, hostIpAddresses)) + existingLobby.HostIps = hostIpAddresses.ToArray(); existingLobby.HostPort = lobbyUpdate.HostPort; + existingLobby.HostTryPort = lobbyUpdate.HostTryPort; + _ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Update, existingLobby)); } } @@ -221,7 +234,7 @@ tcpServer.DataReceived += (clientId, dataLength, data) => { var messageData = bufferRental.Rent(); - var lobbyHostInfo = new LobbyHostInfo() { LobbyId = lobby.Id, HostIp = lobby.HostIp, HostPort = lobby.HostPort }; + var lobbyHostInfo = new LobbyHostInfo() { LobbyId = lobby.Id, HostIps = lobby.HostIps, HostPort = lobby.HostPort, HostTryPort = lobby.HostTryPort }; var messageDataLength = lobbyHostInfo.Serialize(messageData); _ = Task.Run(async () => { @@ -426,7 +439,7 @@ Console.CancelKeyPress += (sender, args) => args.Cancel = true; }; -Console.WriteLine($"{DateTime.Now}: Application started v0.7"); +Console.WriteLine($"{DateTime.Now}: Application started v0.8"); udpServer.Start(8088); tcpServer.Start(8088); diff --git a/LobbyServerDto/LobbyCreate.cs b/LobbyServerDto/LobbyCreate.cs index f3159a6..0a3a34d 100644 --- a/LobbyServerDto/LobbyCreate.cs +++ b/LobbyServerDto/LobbyCreate.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Net; namespace LobbyServerDto { @@ -41,13 +42,17 @@ namespace LobbyServerDto [MaxLength(16)] public byte[]? PasswordSalt { get; set; } /// - /// The hosts ip. Used the the host information send to clients on their request. + /// The hosts ip addresses locally detected. Used the the host information send to clients on their request. /// [MaxLength(32)] - public string? HostIp { get; set; } + public IPAddress[]? HostIps { get; set; } /// /// The hosts port. Used the the host information send to clients on their request. /// public int HostPort { get; set; } + /// + /// The hosts echo port to try if a connection is possible. + /// + public int HostTryPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyHostInfo.cs b/LobbyServerDto/LobbyHostInfo.cs index cbed3aa..83389b1 100644 --- a/LobbyServerDto/LobbyHostInfo.cs +++ b/LobbyServerDto/LobbyHostInfo.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Net; namespace LobbyServerDto { @@ -13,13 +14,17 @@ namespace LobbyServerDto /// public Guid LobbyId { get; set; } /// - /// The hosts ip, this could be an internal address + /// The hosts ip addresses locally detected. Used the the host information send to clients on their request. /// [MaxLength(32)] - public string? HostIp { get; set; } + public IPAddress[]? HostIps { get; set; } /// /// The hosts port /// public int HostPort { get; set; } + /// + /// The hosts echo port to try if a connection is possible. + /// + public int HostTryPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyUpdate.cs b/LobbyServerDto/LobbyUpdate.cs index d9dd5d1..281bf50 100644 --- a/LobbyServerDto/LobbyUpdate.cs +++ b/LobbyServerDto/LobbyUpdate.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Net; namespace LobbyServerDto { @@ -7,7 +8,7 @@ namespace LobbyServerDto /// [LobbyMessage] public partial class LobbyUpdate - { + { /// /// The displayname of the lobby /// @@ -34,13 +35,17 @@ namespace LobbyServerDto [MaxLength(16)] public byte[]? PasswordSalt { get; set; } /// - /// The hosts ip + /// The hosts ip addresses locally detected. Used the the host information send to clients on their request. /// [MaxLength(32)] - public string? HostIp { get; set; } + public IPAddress[]? HostIps { get; set; } /// - /// The hosts port + /// The hosts port. Used the the host information send to clients on their request. /// public int HostPort { get; set; } + /// + /// The hosts echo port to try if a connection is possible. + /// + public int HostTryPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerSourceGenerator/LobbyMessageSourceGenerator.cs b/LobbyServerSourceGenerator/LobbyMessageSourceGenerator.cs index 973b5e4..cf0ff08 100644 --- a/LobbyServerSourceGenerator/LobbyMessageSourceGenerator.cs +++ b/LobbyServerSourceGenerator/LobbyMessageSourceGenerator.cs @@ -74,6 +74,7 @@ namespace LobbyServerDto s.Append(@$"// using System.Collections.Generic; using System.Text; +using System.Net; {(foundClass.Value.nameSpace is null ? null : $@"namespace {foundClass.Value.nameSpace} {{")} @@ -113,6 +114,34 @@ using System.Text; var name = p.Identifier.ToString(); switch(p.Type.ToString()) { + case "IPAddress[]": + case "IPAddress[]?": + s.Append($@" + if ({name} != null) + {{ + int maxLength = Math.Min({name}.Length, {maxLength}); + uint v = (uint)maxLength; + while (v >= 0x80) + {{ + buffer[offset++] = (byte)(v | 0x80); + v >>= 7; + }} + buffer[offset++] = (byte)v; + + for(int i = 0; i < maxLength; i++) + {{ + var ipBuffer = {name}[i].GetAddressBytes(); + buffer[offset++] = (byte)ipBuffer.Length; + Buffer.BlockCopy(ipBuffer, 0, buffer, offset, ipBuffer.Length); + offset += ipBuffer.Length; + }} + }} + else + {{ + buffer[offset++] = 0; + }} +"); + break; case "bool": s.Append($@" buffer[offset++] = (byte)({name} == true ? 1 : 0);"); @@ -226,6 +255,42 @@ using System.Text; var name = p.Identifier.ToString(); switch (p.Type.ToString()) { + case "IPAddress[]": + case "IPAddress[]?": + s.Append($@" + {{ + int arrayLen = 0; + int shift = 0; + byte b; + do {{ + // Check for a corrupted stream. Read a max of 5 bytes. + // In a future version, add a DataFormatException. + if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7 + throw new FormatException(""Format_Bad7BitInt32""); + + // ReadByte handles end of stream cases for us. + b = buffer[offset++]; + arrayLen |= (b & 0x7F) << shift; + shift += 7; + }} while ((b & 0x80) != 0); + + if(arrayLen > {maxLength}) + throw new FormatException(""Format_IPAddressArrayToLong""); + + if(arrayLen > 0) + {{ + ret.{name} = new IPAddress[arrayLen]; + for(int i=0; i < arrayLen; i++) + {{ + var itemLen = buffer[offset++]; + if(itemLen > 16) + throw new FormatException(""Format_IPAddressBytesArrayToLong""); + ret.{name}[i] = new IPAddress(buffer.Slice(offset, itemLen).ToArray()); + offset += itemLen; + }} + }} + }}"); + break; case "bool": s.Append($@" ret.{name} = buffer[offset++] == 0 ? false : true;"); @@ -236,10 +301,10 @@ using System.Text; break; case "Guid": s.Append($@" - {{ - ret.{name} = new Guid(buffer.Slice(offset, 16).ToArray()); - offset+=16; - }}"); + {{ + ret.{name} = new Guid(buffer.Slice(offset, 16).ToArray()); + offset+=16; + }}"); break; case "string": case "string?": @@ -260,6 +325,9 @@ using System.Text; shift += 7; }} while ((b & 0x80) != 0); + if(strLen > {maxLength}) + throw new FormatException(""Format_StringToLong""); + if(strLen > 0) {{ ret.{name} = Encoding.UTF8.GetString(buffer.Slice(offset, strLen).ToArray()); @@ -271,7 +339,7 @@ using System.Text; case "byte[]?": s.Append($@" {{ - int strLen = 0; + int byteLen = 0; int shift = 0; byte b; do {{ @@ -282,14 +350,17 @@ using System.Text; // ReadByte handles end of stream cases for us. b = buffer[offset++]; - strLen |= (b & 0x7F) << shift; + byteLen |= (b & 0x7F) << shift; shift += 7; }} while ((b & 0x80) != 0); - if(strLen > 0) + if(byteLen > {maxLength}) + throw new FormatException(""Format_ByteArrayToLong""); + + if(byteLen > 0) {{ - ret.{name} = buffer.Slice(offset, strLen).ToArray(); - offset += strLen; + ret.{name} = buffer.Slice(offset, byteLen).ToArray(); + offset += byteLen; }} }}"); break; diff --git a/LobbyServerSourceGenerator/LobbyServerSourceGenerator.csproj b/LobbyServerSourceGenerator/LobbyServerSourceGenerator.csproj index 8845a69..a6941ba 100644 --- a/LobbyServerSourceGenerator/LobbyServerSourceGenerator.csproj +++ b/LobbyServerSourceGenerator/LobbyServerSourceGenerator.csproj @@ -15,10 +15,6 @@ - - - -