commit 6de712910f0edca132d1824559d5c97e32cfe8f5 Author: Timerix Date: Tue Oct 22 13:16:20 2024 +0500 Ping! Pong! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c25fee1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Build results +[Bb]in/ +.bin/ +[Dd]ebug/ +[Rr]elease/ +[Rr]eleases/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ +[Pp]ublish/ + +# IDE files +.vs/ +.vscode/ +.vshistory/ +.idea/ +с/ +.editorconfig +*.user + +#backups +.old*/ \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..4479e04 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,12 @@ + + + net8.0 + latest + True + disable + enable + true + + CA1416 + + diff --git a/Meum.Client.CLI/Meum.Client.CLI.csproj b/Meum.Client.CLI/Meum.Client.CLI.csproj new file mode 100644 index 0000000..836657c --- /dev/null +++ b/Meum.Client.CLI/Meum.Client.CLI.csproj @@ -0,0 +1,14 @@ + + + Exe + 1.0.0 + + + + + + + + + + diff --git a/Meum.Client.CLI/Program.cs b/Meum.Client.CLI/Program.cs new file mode 100644 index 0000000..f6b04ec --- /dev/null +++ b/Meum.Client.CLI/Program.cs @@ -0,0 +1,83 @@ +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +global using Meum.Core; +using System.Net; +using System.Reflection; +using DTLib.Console; +using DTLib.Demystifier; +using DTLib.Extensions; + +namespace Meum.Client.CLI; + +class Program +{ + private const string greeting_art = + """ + ^,,^ ╱| + ( •·•) Meum! (o.o`7 + / ` | Meum... |`˜ \ + \(_,J J L l`,)/ + """; + + private const string farewell_art = + """ + ^,,^ ╱| + ( -.-) (>,<`7 + / ` | Goodbye! |`˜ \ + \(_,J J L l`,)/ + """; + + static async Task Main(string[] args) + { + Console.OutputEncoding = StringConverter.UTF8; + Console.InputEncoding = StringConverter.UTF8; + ColoredConsole.ResetColor(); + var v = Assembly.GetExecutingAssembly().GetName().Version; + string title = $"Meum CLI v{v?.ToString(3) ?? "Null"}"; + Console.Title = title; + ColoredConsole.WriteTitle(title, '=', fg: ConsoleColor.Cyan); + ColoredConsole.WriteLine(greeting_art, fg: ConsoleColor.Magenta); + ColoredConsole.WriteHLine('=', fg: ConsoleColor.Cyan); + + UserAddress? userAddress = null; + DnsEndPoint? serverEndPoint = null; + string? serverAddress = null; + while (true) + { + try + { + if(userAddress == null) + { + var addrstr = ColoredConsole.ReadLine("enter user address (name@server.xyz)", ConsoleColor.Blue); + if (string.IsNullOrEmpty(addrstr)) + continue; + userAddress = new(addrstr); + } + Client client = new(userAddress); + if(serverEndPoint == null) + { + serverAddress = ColoredConsole.ReadLine("enter server address (server.xyz)", ConsoleColor.Blue); + if (string.IsNullOrEmpty(serverAddress)) + { + ColoredConsole.WriteLine("null address", ConsoleColor.Red); + continue; + } + serverEndPoint = Network.ParseDnsEndPoint(serverAddress); + ColoredConsole.WriteTitle(serverAddress, fg: ConsoleColor.Cyan); + } + + ColoredConsole.WriteLine("connecting to the server...", ConsoleColor.Blue); + var conn = await client.ConnectToServerAsync(serverEndPoint); + await conn.PingAsync(); + await Task.Delay(-1); + } + catch (Exception ex) + { + ColoredConsole.WriteLine(ex.ToStringDemystified(), ConsoleColor.Red); + } + } + // ColoredConsole.ResetColor(); + } +} diff --git a/Meum.Client/Client.cs b/Meum.Client/Client.cs new file mode 100644 index 0000000..7589d66 --- /dev/null +++ b/Meum.Client/Client.cs @@ -0,0 +1,29 @@ +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +global using Meum.Core; +using System.Net; + +namespace Meum.Client; + +public class Client +{ + private readonly HashSet _connectedServers = new(); + public IReadOnlySet ConnectedServers => _connectedServers; + + public UserAddress Address { get; } + + public Client(UserAddress address) + { + Address = address; + } + + public async Task ConnectToServerAsync(DnsEndPoint serverEndPoint) + { + var serv = new ServerConnection(serverEndPoint); + await serv.ConnectAsync(); + _connectedServers.Add(serv); + return serv; + } +} \ No newline at end of file diff --git a/Meum.Client/Meum.Client.csproj b/Meum.Client/Meum.Client.csproj new file mode 100644 index 0000000..b511aa1 --- /dev/null +++ b/Meum.Client/Meum.Client.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Meum.Client/ServerConnection.cs b/Meum.Client/ServerConnection.cs new file mode 100644 index 0000000..9305677 --- /dev/null +++ b/Meum.Client/ServerConnection.cs @@ -0,0 +1,47 @@ +using System.Net; +using System.Net.Quic; +using System.Net.Security; +using DTLib.Extensions; + +namespace Meum.Client; + +public class ServerConnection : IDisposable +{ + public DnsEndPoint ServerEndPoint { get; } + + private QuicConnection? _quicConnection; + + public ServerConnection(DnsEndPoint serverEndPoint) + { + ServerEndPoint = serverEndPoint; + } + + public async Task ConnectAsync() + { + _quicConnection = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions + { + RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, Network.ServerPortDefault), + DefaultStreamErrorCode = Network.DefaultStreamErrorCode, + DefaultCloseErrorCode = Network.DefaultCloseErrorCode, + ClientAuthenticationOptions = new SslClientAuthenticationOptions + { + ApplicationProtocols = Network.ApplicationProtocols.ToList(), + TargetHost = ServerEndPoint.Host + } + }); + } + + public async Task PingAsync() + { + var stream = await _quicConnection!.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); + await stream.WriteAsync("Ping\n".ToBytes()); + StreamReader reader = new StreamReader(stream); + string line = await reader.ReadLineAsync() ?? String.Empty; + Console.WriteLine(line); + } + + public void Dispose() + { + _quicConnection?.DisposeAsync(); + } +} \ No newline at end of file diff --git a/Meum.Core/Meum.Core.csproj b/Meum.Core/Meum.Core.csproj new file mode 100644 index 0000000..0d100d8 --- /dev/null +++ b/Meum.Core/Meum.Core.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Meum.Core/Network.cs b/Meum.Core/Network.cs new file mode 100644 index 0000000..c0a72c7 --- /dev/null +++ b/Meum.Core/Network.cs @@ -0,0 +1,47 @@ +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +using System.Net; +using System.Net.Security; + +namespace Meum.Core; + +public static class Network +{ + public static readonly SslApplicationProtocol[] ApplicationProtocols = + [ + new("Meum-1") + ]; + + public const int ServerPortDefault = 9320; + public const long DefaultStreamErrorCode = 0xA; + public const long DefaultCloseErrorCode = 0xB; + + + public static bool IsValidDomainName(string name) + { + return Uri.CheckHostName(name) != UriHostNameType.Unknown; + } + + public static DnsEndPoint ParseDnsEndPoint(string address_str) + { + string host; + int port; + int colon_index = address_str.IndexOf(':'); + if (colon_index == -1) + { + host = address_str; + port = ServerPortDefault; + } + else + { + host = address_str.Substring(0, colon_index); + port = Convert.ToInt32(address_str.Substring(colon_index + 1)); + } + if(!IsValidDomainName(host)) + throw new ArgumentException($"Invalid domain name '{host}'"); + + return new DnsEndPoint(host, port); + } +} \ No newline at end of file diff --git a/Meum.Core/UserAddress.cs b/Meum.Core/UserAddress.cs new file mode 100644 index 0000000..e56434e --- /dev/null +++ b/Meum.Core/UserAddress.cs @@ -0,0 +1,48 @@ +using System.Net; + +namespace Meum.Core; + +public class UserAddress +{ + public readonly string Name; + public readonly DnsEndPoint RegistrationServer; + public readonly string Full; + + public UserAddress(string name, DnsEndPoint registrationServer) + { + Name = name; + RegistrationServer = registrationServer; + Full = $"{name}@{registrationServer}"; + } + + public UserAddress(string addrstr) + { + int at_index = addrstr.IndexOf('@'); + if(at_index == -1) + throw new FormatException($"Invalid user address format '{addrstr}'"); + Full = addrstr; + Name = addrstr.Substring(0, at_index); + if(!ValidateName(Name)) + throw new FormatException($"Invalid user name '{Name}' in address '{addrstr}'"); + string serverstr = addrstr.Substring(at_index + 1); + + RegistrationServer = Network.ParseDnsEndPoint(serverstr); + } + + public override string ToString() => Full; + + public override int GetHashCode() => ToString().GetHashCode(); + + private static bool ValidateName(string? name) + { + if(string.IsNullOrEmpty(name)) + return false; + foreach (char c in name) + { + if(!char.IsAsciiLetterOrDigit(c) && c != '.' && c != '-' && c != '_') + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Meum.Server/Config.cs b/Meum.Server/Config.cs new file mode 100644 index 0000000..dff99c2 --- /dev/null +++ b/Meum.Server/Config.cs @@ -0,0 +1,11 @@ +using Meum.Core; + +namespace Meum.Server; + +public class Config +{ + public string listener_ip { get; set; } = "127.0.0.1"; + public int listener_port { get; set; } = Network.ServerPortDefault; + public string certificate_path { get; set; } = "self-signed.pem"; + public string? key_path { get; set; } = "self-signed.key"; +} \ No newline at end of file diff --git a/Meum.Server/Meum.Server.csproj b/Meum.Server/Meum.Server.csproj new file mode 100644 index 0000000..34bd7d1 --- /dev/null +++ b/Meum.Server/Meum.Server.csproj @@ -0,0 +1,13 @@ + + + Exe + + + + + + + + + + diff --git a/Meum.Server/Program.cs b/Meum.Server/Program.cs new file mode 100644 index 0000000..3d2f318 --- /dev/null +++ b/Meum.Server/Program.cs @@ -0,0 +1,70 @@ +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +global using Meum.Core; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Quic; +using System.Net.Security; +using System.Runtime.Serialization; +using System.Security.Cryptography.X509Certificates; +using DTLib.Console; +using DTLib.Demystifier; +using DTLib.Extensions; + +namespace Meum.Server; + +class Program +{ + static async Task Main(string[] args) + { + try + { + var config = new Config(); + var certificate = X509Certificate2.CreateFromPemFile(config.certificate_path, config.key_path); + if(!certificate.Verify()) + throw new Exception("Certificate is not valid"); + var serverConnectionOptions = new QuicServerConnectionOptions + { + DefaultStreamErrorCode = Network.DefaultStreamErrorCode, + DefaultCloseErrorCode = Network.DefaultCloseErrorCode, + ServerAuthenticationOptions = new SslServerAuthenticationOptions + { + ApplicationProtocols = Network.ApplicationProtocols.ToList(), + ServerCertificate = certificate, + ClientCertificateRequired = false + } + }; + var listenerOptions = new QuicListenerOptions + { + ListenEndPoint = new IPEndPoint(IPAddress.Parse(config.listener_ip), config.listener_port), + ApplicationProtocols = Network.ApplicationProtocols.ToList(), + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(serverConnectionOptions) + }; + var listener = await QuicListener.ListenAsync(listenerOptions); + while (true) + { + try + { + var conn = await listener.AcceptConnectionAsync(); + var stream = await conn.AcceptInboundStreamAsync(); + StreamReader reader = new(stream); + string line = await reader.ReadLineAsync() ?? ""; + Console.WriteLine(line); + await stream.WriteAsync("Pong\n".ToBytes()); + await conn.CloseAsync(Network.DefaultCloseErrorCode); + } + catch (Exception ex) + { + ColoredConsole.WriteLine(ex.ToStringDemystified(), ConsoleColor.Red); + } + } + } + catch (Exception ex) + { + ColoredConsole.WriteLine(ex.ToStringDemystified(), ConsoleColor.Red); + } + } +} \ No newline at end of file diff --git a/Meum.Server/README.md b/Meum.Server/README.md new file mode 100644 index 0000000..f9885a0 --- /dev/null +++ b/Meum.Server/README.md @@ -0,0 +1,4 @@ +## Create self-signed certificate +```sh +dotnet dev-certs https -ep bin/Debug/net8.0/self-signed.pfx --trust --format PEM --no-password +``` \ No newline at end of file diff --git a/Meum.sln b/Meum.sln new file mode 100644 index 0000000..fffdd67 --- /dev/null +++ b/Meum.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meum.Server", "Meum.Server\Meum.Server.csproj", "{769D657F-488E-4794-AC12-F7894037CB84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meum.Client.CLI", "Meum.Client.CLI\Meum.Client.CLI.csproj", "{1DA062F3-F4F6-4A67-AC66-281EEA7427C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meum.Core", "Meum.Core\Meum.Core.csproj", "{2A31C5C6-A8A8-4C7F-B913-1F6BBD906743}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution_items", "solution_items", "{9375C0E5-DB78-4E77-869B-F9F7DC2651D1}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meum.Client", "Meum.Client\Meum.Client.csproj", "{6DADE8A1-B363-4888-BB9B-72282B9AC769}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {769D657F-488E-4794-AC12-F7894037CB84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {769D657F-488E-4794-AC12-F7894037CB84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {769D657F-488E-4794-AC12-F7894037CB84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {769D657F-488E-4794-AC12-F7894037CB84}.Release|Any CPU.Build.0 = Release|Any CPU + {1DA062F3-F4F6-4A67-AC66-281EEA7427C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DA062F3-F4F6-4A67-AC66-281EEA7427C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DA062F3-F4F6-4A67-AC66-281EEA7427C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DA062F3-F4F6-4A67-AC66-281EEA7427C6}.Release|Any CPU.Build.0 = Release|Any CPU + {2A31C5C6-A8A8-4C7F-B913-1F6BBD906743}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A31C5C6-A8A8-4C7F-B913-1F6BBD906743}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A31C5C6-A8A8-4C7F-B913-1F6BBD906743}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A31C5C6-A8A8-4C7F-B913-1F6BBD906743}.Release|Any CPU.Build.0 = Release|Any CPU + {6DADE8A1-B363-4888-BB9B-72282B9AC769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DADE8A1-B363-4888-BB9B-72282B9AC769}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DADE8A1-B363-4888-BB9B-72282B9AC769}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DADE8A1-B363-4888-BB9B-72282B9AC769}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal