mirror of
https://github.com/AlexMacocian/MTSC.git
synced 2026-07-15 14:59:33 +00:00
Implemented client encryption handler
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<RootNamespace>MTSC_TestServer</RootNamespace>
|
||||
<StartupObject>MTSC_TestServer.Program</StartupObject>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using MTSC.Server;
|
||||
using MTSC.Server.Handlers;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace MTSC_TestServer
|
||||
{
|
||||
@@ -8,7 +10,9 @@ namespace MTSC_TestServer
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Server server = new Server(555);
|
||||
server.
|
||||
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(1024);
|
||||
EncryptionHandler encryptionHandler = new EncryptionHandler(rsa);
|
||||
server.AddHandler(encryptionHandler).Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,32 @@ VisualStudioVersion = 16.0.29102.190
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTSC", "MTSC\MTSC.csproj", "{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MTSC-TestServer", "MTSC-TestServer\MTSC-TestServer.csproj", "{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTSC-TestServer", "MTSC-TestServer\MTSC-TestServer.csproj", "{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Debug|x64.Build.0 = Debug|x64
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Release|x64.ActiveCfg = Release|x64
|
||||
{869DAEBC-5884-43C9-BA0B-2C4376A7ED1C}.Release|x64.Build.0 = Release|x64
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Debug|x64.Build.0 = Debug|x64
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Release|x64.ActiveCfg = Release|x64
|
||||
{08A53F8F-BA6B-4C1C-B24B-CFD2626DE6C3}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace MTSC.Client
|
||||
*/
|
||||
foreach(IHandler handler in handlers)
|
||||
{
|
||||
if(handler.PreHandleReceivedMessage(tcpClient, out message))
|
||||
if(handler.PreHandleReceivedMessage(tcpClient, ref message))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
namespace MTSC.Client.Handlers
|
||||
{
|
||||
public class EncryptionHandler : IHandler
|
||||
{
|
||||
private enum ConnectionState
|
||||
{
|
||||
Initial,
|
||||
RequestingPublicKey,
|
||||
NegotiatingSymKey,
|
||||
Encrypted
|
||||
}
|
||||
|
||||
private ConnectionState connectionState = ConnectionState.Initial;
|
||||
private byte[] aesKey;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize an encrypted connection.
|
||||
/// </summary>
|
||||
/// <param name="client">TcpClient connection.</param>
|
||||
/// <returns>True if connection is successful. False otherwise.</returns>
|
||||
public bool InitializeConnection(TcpClient client)
|
||||
{
|
||||
this.connectionState = ConnectionState.Initial;
|
||||
return true;
|
||||
}
|
||||
/// <summary>
|
||||
/// Handles the operations done after client disconnected.
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
public void Disconnected(TcpClient client)
|
||||
{
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Performs operations on the message before sending it.
|
||||
/// </summary>
|
||||
/// <param name="client">Client connection.</param>
|
||||
/// <param name="message">Message to be processed.</param>
|
||||
/// <returns>True if no other handler should process the message further.</returns>
|
||||
public bool HandleSendMessage(TcpClient client, ref Message message)
|
||||
{
|
||||
if(connectionState == ConnectionState.Encrypted)
|
||||
{
|
||||
byte[] decryptedBytes = message.MessageBytes;
|
||||
byte[] encryptedBytes = EncryptBytes(decryptedBytes);
|
||||
message = CommunicationPrimitives.BuildMessage(encryptedBytes);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Performs operations on the message buffer, modifying it.
|
||||
/// </summary>
|
||||
/// <param name="client">Client connection.</param>
|
||||
/// <param name="message">Message to be processed.</param>
|
||||
/// <returns>True if no other handler should process it further.</returns>
|
||||
public bool PreHandleReceivedMessage(TcpClient client, ref Message message)
|
||||
{
|
||||
if(connectionState == ConnectionState.Encrypted)
|
||||
{
|
||||
byte[] encryptedBytes = message.MessageBytes;
|
||||
byte[] decryptedBytes = DecryptBytes(encryptedBytes);
|
||||
message = CommunicationPrimitives.BuildMessage(decryptedBytes);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Handle the received message.
|
||||
/// </summary>
|
||||
/// <param name="client">Client connection.</param>
|
||||
/// <param name="message">Message.</param>
|
||||
/// <returns>True if no other handler should handle the message further.</returns>
|
||||
public bool HandleReceivedMessage(TcpClient client, Message message)
|
||||
{
|
||||
if(connectionState == ConnectionState.RequestingPublicKey)
|
||||
{
|
||||
string ascii = ASCIIEncoding.ASCII.GetString(message.MessageBytes);
|
||||
if (ascii.Contains(CommunicationPrimitives.SendPublicKey))
|
||||
{
|
||||
byte[] publicKeyBytes = new byte[message.MessageLength - CommunicationPrimitives.SendPublicKey.Length - 1];
|
||||
Array.Copy(message.MessageBytes, message.MessageLength + 1, publicKeyBytes, 0, publicKeyBytes.Length);
|
||||
string publicKey = ASCIIEncoding.ASCII.GetString(publicKeyBytes);
|
||||
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(1024);
|
||||
RSAParameters parameters = new RSAParameters();
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(publicKey);
|
||||
if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue"))
|
||||
{
|
||||
foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes)
|
||||
{
|
||||
switch (node.Name)
|
||||
{
|
||||
case "Modulus": parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "Exponent": parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "P": parameters.P = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "Q": parameters.Q = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "DP": parameters.DP = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "DQ": parameters.DQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "InverseQ": parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
case "D": parameters.D = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
rsa.ImportParameters(parameters);
|
||||
byte[] symkeyBytes = GetUniqueByteKey(32);
|
||||
byte[] encryptedSymKey = rsa.Encrypt(symkeyBytes, false);
|
||||
aesKey = symkeyBytes;
|
||||
byte[] messageHeader = ASCIIEncoding.ASCII.GetBytes(CommunicationPrimitives.SendEncryptionKey + ":");
|
||||
byte[] messageBytes = new byte[encryptedSymKey.Length + messageHeader.Length];
|
||||
Array.Copy(messageHeader, 0, messageBytes, 0, messageHeader.Length);
|
||||
Array.Copy(encryptedSymKey, 0, messageBytes, messageHeader.Length, encryptedSymKey.Length);
|
||||
Message sendMessage = CommunicationPrimitives.BuildMessage(messageBytes);
|
||||
CommunicationPrimitives.SendMessage(client, sendMessage);
|
||||
connectionState = ConnectionState.NegotiatingSymKey;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if(connectionState == ConnectionState.NegotiatingSymKey)
|
||||
{
|
||||
string ascii = ASCIIEncoding.ASCII.GetString(message.MessageBytes);
|
||||
if(ascii == CommunicationPrimitives.AcceptEncryptionKey)
|
||||
{
|
||||
connectionState = ConnectionState.Encrypted;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionState = ConnectionState.Initial;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Called on every tick by the client object.
|
||||
/// Performs regular operations.
|
||||
/// </summary>
|
||||
/// <param name="client">Client connection.</param>
|
||||
public void Tick(TcpClient client)
|
||||
{
|
||||
if(connectionState == ConnectionState.Initial)
|
||||
{
|
||||
Message message = CommunicationPrimitives.BuildMessage(ASCIIEncoding.ASCII.GetBytes(CommunicationPrimitives.RequestPublicKey));
|
||||
CommunicationPrimitives.SendMessage(client, message);
|
||||
this.connectionState = ConnectionState.RequestingPublicKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GetUniqueKey(int size)
|
||||
{
|
||||
return Convert.ToBase64String(GetUniqueByteKey(size));
|
||||
}
|
||||
|
||||
private byte[] GetUniqueByteKey(int size)
|
||||
{
|
||||
byte[] data = new byte[size];
|
||||
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
|
||||
{
|
||||
crypto.GetBytes(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private byte[] EncryptBytes(byte[] bytesToBeEncrypted)
|
||||
{
|
||||
byte[] encryptedBytes = null;
|
||||
|
||||
// Set your salt here, change it to meet your flavor:
|
||||
// The salt bytes must be at least 8 bytes.
|
||||
byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
using (RijndaelManaged AES = new RijndaelManaged())
|
||||
{
|
||||
AES.KeySize = 256;
|
||||
AES.BlockSize = 128;
|
||||
AES.Mode = CipherMode.CBC;
|
||||
var key = new Rfc2898DeriveBytes(aesKey, saltBytes, 1000);
|
||||
AES.Key = key.GetBytes(AES.KeySize / 8);
|
||||
AES.IV = key.GetBytes(AES.BlockSize / 8);
|
||||
|
||||
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
|
||||
cs.Close();
|
||||
}
|
||||
encryptedBytes = ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedBytes;
|
||||
}
|
||||
|
||||
private byte[] DecryptBytes(byte[] bytesToBeDecrypted)
|
||||
{
|
||||
byte[] decryptedBytes = null;
|
||||
|
||||
// Set your salt here, change it to meet your flavor:
|
||||
// The salt bytes must be at least 8 bytes.
|
||||
byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
using (RijndaelManaged AES = new RijndaelManaged())
|
||||
{
|
||||
AES.KeySize = 256;
|
||||
AES.BlockSize = 128;
|
||||
|
||||
AES.Mode = CipherMode.CBC;
|
||||
|
||||
var key = new Rfc2898DeriveBytes(aesKey, saltBytes, 1000);
|
||||
AES.Key = key.GetBytes(AES.KeySize / 8);
|
||||
AES.IV = key.GetBytes(AES.BlockSize / 8);
|
||||
|
||||
|
||||
|
||||
using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
|
||||
cs.Close();
|
||||
}
|
||||
decryptedBytes = ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
private string EncryptText(string input)
|
||||
{
|
||||
// Get the bytes of the string
|
||||
byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
|
||||
|
||||
byte[] bytesEncrypted = EncryptBytes(bytesToBeEncrypted);
|
||||
|
||||
string result = Encoding.UTF8.GetString(bytesEncrypted);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string DecryptText(string input)
|
||||
{
|
||||
// Get the bytes of the string
|
||||
byte[] bytesToBeDecrypted = Encoding.UTF8.GetBytes(input);
|
||||
|
||||
byte[] bytesDecrypted = DecryptBytes(bytesToBeDecrypted);
|
||||
|
||||
string result = Encoding.UTF8.GetString(bytesDecrypted);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace MTSC.Client.Handlers
|
||||
/// <param name="client">Client socket.</param>
|
||||
/// <param name="message">Message to be sent.</param>
|
||||
/// <returns></returns>
|
||||
bool OnSend(TcpClient client, out Message message);
|
||||
bool HandleSendMessage(TcpClient client, ref Message message);
|
||||
/// <summary>
|
||||
/// Called before the message is handled.
|
||||
/// Use this method to modify the message if necesarry.
|
||||
@@ -31,7 +31,7 @@ namespace MTSC.Client.Handlers
|
||||
/// <param name="client">Client object.</param>
|
||||
/// <param name="message">Message to be preprocessed.</param>
|
||||
/// <returns>True if the message has been preprocessed and no other handler should modify the message.</returns>
|
||||
bool PreHandleReceivedMessage(TcpClient client, out Message message);
|
||||
bool PreHandleReceivedMessage(TcpClient client, ref Message message);
|
||||
/// <summary>
|
||||
/// Called when a message has been received.
|
||||
/// </summary>
|
||||
@@ -48,6 +48,6 @@ namespace MTSC.Client.Handlers
|
||||
/// Called when the client is disconnected.
|
||||
/// </summary>
|
||||
/// <param name="client">Client object.</param>
|
||||
void OnDisconnect(TcpClient client);
|
||||
void Disconnected(TcpClient client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<AssemblyVersion>0.0.5.0</AssemblyVersion>
|
||||
<FileVersion>0.0.5.0</FileVersion>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -38,22 +38,35 @@ namespace MTSC.Server.Handlers
|
||||
public EncryptionHandler(RSACryptoServiceProvider rsa)
|
||||
{
|
||||
additionalData = new Dictionary<ClientStruct, AdditionalData>();
|
||||
privateKey = rsa.ToXmlString(true);
|
||||
publicKey = rsa.ToXmlString(false);
|
||||
privateKey = HelperFunctions.ToXmlString(rsa, true);
|
||||
publicKey = HelperFunctions.ToXmlString(rsa, false);
|
||||
}
|
||||
#endregion
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Called when a client is being removed.
|
||||
/// </summary>
|
||||
/// <param name="client">Client to be removed.</param>
|
||||
public void ClientRemoved(ClientStruct client)
|
||||
{
|
||||
additionalData.Remove(client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a new client connection.
|
||||
/// </summary>
|
||||
/// <param name="client">New client connection.</param>
|
||||
/// <returns>False if an error occurred.</returns>
|
||||
public bool HandleClient(ClientStruct client)
|
||||
{
|
||||
additionalData.Add(client, new AdditionalData());
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a received message.
|
||||
/// </summary>
|
||||
/// <param name="client">Client connection.</param>
|
||||
/// <param name="message">Received message.</param>
|
||||
/// <returns>True if no other handler should handle current message.</returns>
|
||||
public bool HandleMessage(ClientStruct client, Message message)
|
||||
{
|
||||
if (additionalData[client].ClientState == ClientState.Initial || additionalData[client].ClientState == ClientState.Negotiating)
|
||||
@@ -68,8 +81,8 @@ namespace MTSC.Server.Handlers
|
||||
}
|
||||
else if (asciiMessage.Contains(CommunicationPrimitives.SendEncryptionKey))
|
||||
{
|
||||
byte[] encryptedKey = new byte[message.MessageLength - CommunicationPrimitives.SendEncryptionKey.Length];
|
||||
Array.Copy(message.MessageBytes, CommunicationPrimitives.SendEncryptionKey.Length, encryptedKey, 0, encryptedKey.Length);
|
||||
byte[] encryptedKey = new byte[message.MessageLength - CommunicationPrimitives.SendEncryptionKey.Length - 1];
|
||||
Array.Copy(message.MessageBytes, CommunicationPrimitives.SendEncryptionKey.Length + 1, encryptedKey, 0, encryptedKey.Length);
|
||||
byte[] decryptedKey = rsa.Decrypt(encryptedKey, false);
|
||||
additionalData[client].Key = decryptedKey;
|
||||
Message sendMessage = CommunicationPrimitives.BuildMessage(ASCIIEncoding.ASCII.GetBytes(CommunicationPrimitives.AcceptEncryptionKey));
|
||||
@@ -87,7 +100,12 @@ namespace MTSC.Server.Handlers
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform transformative operations on the message.
|
||||
/// </summary>
|
||||
/// <param name="client">Client connection.</param>
|
||||
/// <param name="message">Message to be processed.</param>
|
||||
/// <returns>True if no other handlers should perform operations on current message.</returns>
|
||||
public bool PreHandleMessage(ClientStruct client, ref Message message)
|
||||
{
|
||||
if(additionalData[client].ClientState == ClientState.Encrypted)
|
||||
@@ -108,7 +126,9 @@ namespace MTSC.Server.Handlers
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs periodic operations on the server.
|
||||
/// </summary>
|
||||
public void Tick()
|
||||
{
|
||||
|
||||
|
||||
Reference in New Issue
Block a user