Ping! Pong!

This commit is contained in:
Timerix 2024-10-22 13:16:20 +05:00
commit 6de712910f
15 changed files with 462 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -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*/

12
Directory.Build.props Normal file
View File

@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- System.Net.Quic not officially supported on some platforms -->
<NoWarn>CA1416</NoWarn>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<Version>1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Meum.Client\Meum.Client.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DTLib" Version="1.6.0" />
</ItemGroup>
</Project>

View File

@ -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();
}
}

29
Meum.Client/Client.cs Normal file
View File

@ -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<ServerConnection> _connectedServers = new();
public IReadOnlySet<ServerConnection> ConnectedServers => _connectedServers;
public UserAddress Address { get; }
public Client(UserAddress address)
{
Address = address;
}
public async Task<ServerConnection> ConnectToServerAsync(DnsEndPoint serverEndPoint)
{
var serv = new ServerConnection(serverEndPoint);
await serv.ConnectAsync();
_connectedServers.Add(serv);
return serv;
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Meum.Core\Meum.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DTLib" Version="1.6.0" />
</ItemGroup>
</Project>

View File

@ -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();
}
}

View File

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Unofficial.MsQuic" Version="2.4.6" />
</ItemGroup>
</Project>

47
Meum.Core/Network.cs Normal file
View File

@ -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);
}
}

48
Meum.Core/UserAddress.cs Normal file
View File

@ -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;
}
}

11
Meum.Server/Config.cs Normal file
View File

@ -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";
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Meum.Core\Meum.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DTLib" Version="1.6.0" />
</ItemGroup>
</Project>

70
Meum.Server/Program.cs Normal file
View File

@ -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);
}
}
}

4
Meum.Server/README.md Normal file
View File

@ -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
```

39
Meum.sln Normal file
View File

@ -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