Home
Mats Vederhus edited this page Aug 27, 2023
·
17 revisions
Welcome to the Parlo wiki!
Here's an example of how to use Parlo to create a safe and secure communication protocol. The following example uses SRP.NET and ZeroFormatter for this purpose. They are available as nuGet packages.
using ZeroFormatter;
namespace ParloProtocol
{
public enum AuthPacketIDs
{
ClientSignup = 0x00,
ClientInitialAuth = 0x01,
ServerInitialAuthResponse = 0x02,
CAuthProof = 0x03, //Sent by client
SAuthProof = 0x04 //Sent by server
}
/// <summary>
/// The packet sent by the client to the server (may or may not be a different server from the login server)
/// to sign up (I.E create an account in the DB).
/// </summary>
[ZeroFormattable]
public struct ClientSignup : IPacket
{
public ClientSignup(string userName, string salt, string verifier)
{
Username = userName;
Salt = salt;
Verifier = verifier;
}
[Index(0)]
public string Username = default!;
[Index(1)]
public string Salt = default!;
[Index(2)]
public string Verifier = default!;
}
/// <summary>
/// The packet sent by the client to the server to initiate authentication.
/// </summary>
[ZeroFormattable]
public struct ClientInitialAuth : IPacket
{
public ClientInitialAuth(string userName, string ephemeral)
{
Username = userName;
Ephemeral = ephemeral;
}
[Index(0)]
public string Username = default!;
[Index(1)]
public string Ephemeral = default!;
}
/// <summary>
/// The initial response from the server.
/// </summary>
[ZeroFormattable]
public struct ServerInitialAuthResponse : IPacket
{
public ServerInitialAuthResponse(string salt, string publicEphemeral)
{
Salt = salt;
PublicEphemeral = publicEphemeral;
}
[Index(0)]
public string Salt = default!;
[Index(1)]
public string PublicEphemeral = default!;
}
/// <summary>
/// Sent by the client to the server and vice versa.
/// </summary>
[ZeroFormattable]
public struct AuthProof : IPacket
{
public AuthProof(string sessionProof)
{
SessionProof = sessionProof;
}
[Index(0)]
public string SessionProof = default!;
}
}
ClientNetworkManager:
using Parlo;
using Parlo.Packets;
using SecureRemotePassword;
using ZeroFormatter;
namespace ParloProtocol
{
public delegate Task OnPacketReceivedDelegate(IPacket Packet, byte ID, NetworkClient Sender);
public class ClientNetworkManager
{
private static Lazy<ClientNetworkManager> m_Instance = new Lazy<ClientNetworkManager>(() => new ClientNetworkManager());
private static NetworkClient m_Client = default!;
private static SrpClient m_SRPClient = new SrpClient();
private static SrpEphemeral m_ClientEphemeral = default!;
private static Task m_NetworkingTask = default!;
private static bool m_HasBeenAuthenticated = false;
/// <summary>
/// Has the client been authenticated?
/// This means it should start sending
/// encrypted packets to the server.
/// </summary>
public static bool HasBeenAuthenticated { get { return m_HasBeenAuthenticated; } }
/// <summary>
/// Event invoked when the client has connected.
/// </summary>
public static event OnConnectedDelegate OnConnected = default!;
/// <summary>
/// Event invoked when a network error occurred.
/// </summary>
public static event NetworkErrorDelegate OnNetworkError = default!;
/// <summary>
/// Event invoked when the client received a packet.
/// </summary>
public static event OnPacketReceivedDelegate OnPacketReceived = default!;
/// <summary>
/// Gets an instance of this ClientNetworkManager.
/// </summary>
public static ClientNetworkManager Instance { get { return m_Instance.Value; } }
/// <summary>
/// Connects to a remote server.
/// </summary>
/// <param name="IP">The IP to connect to.</param>
/// <param name="Port">The port to use.</param>
/// <param name="Username">The player's username.</param>
/// <param name="Password">The player's password.</param>
public void Connect(string IP, int Port, string Username, string Password)
{
m_Client = new NetworkClient(new ParloSocket(true));
m_Client.OnConnected += Client_OnConnected;
m_Client.OnNetworkError += Client_OnNetworkError;
m_Client.OnReceivedData += Client_OnReceivedData;
LoginArgsContainer LoginArgs = new LoginArgsContainer();
LoginArgs.Address = IP;
LoginArgs.Port = Port;
LoginArgs.Client = m_Client;
LoginArgs.Username = Username;
LoginArgs.Password = Password;
if (PacketHandlers.Get((byte)AuthPacketIDs.ServerInitialAuthResponse) == null)
{
PacketHandlers.Register((byte)AuthPacketIDs.ServerInitialAuthResponse, false,
new OnPacketReceived(Client_OnReceivedData));
}
m_NetworkingTask = Task.Run(async () => { await m_Client.ConnectAsync(LoginArgs); });
}
/// <summary>
/// Sends a packet to the server.
/// </summary>
/// <param name="Packet">The packet to send.</param>
public Task SendAsync(Packet P)
{
return m_Client.SendAsync(P.BuildPacket());
}
/// <summary>
/// Sends a encrypted packet to the server.
/// </summary>
/// <param name="Packet">The encrypted packet to send.</param>
public Task SendAsync(EncryptedPacket P)
{
if (!m_HasBeenAuthenticated)
throw new InvalidOperationException("Cannot send encrypted packets before authentication.");
return m_Client.SendAsync(P.BuildPacket());
}
/// <summary>
/// The client connected to the given server.
/// This starts the SRP6 authentication process by sending an initial packet.
/// </summary>
/// <param name="LoginArgs">The LoginArgsContainer created based on the args passed to Connect().</param>
private async Task Client_OnConnected(LoginArgsContainer LoginArgs)
{
await OnConnected?.Invoke(LoginArgs);
ClientInitialAuth InitialAuth = new ClientInitialAuth();
InitialAuth.Username = LoginArgs.Username;
string Salt = m_SRPClient.GenerateSalt();
string PrivateKey = m_SRPClient.DerivePrivateKey(Salt, LoginArgs.Username, LoginArgs.Password);
string Verifier = m_SRPClient.DeriveVerifier(PrivateKey);
m_ClientEphemeral = m_SRPClient.GenerateEphemeral();
InitialAuth.Ephemeral = m_ClientEphemeral.Public;
byte[] Data = ZeroFormatterSerializer.Serialize(InitialAuth);
}
/// <summary>
/// A network error occurred!
/// </summary>
/// <param name="Exception">The SocketException that was thrown.</param>
private void Client_OnNetworkError(System.Net.Sockets.SocketException Exception)
{
OnNetworkError?.Invoke(Exception);
}
/// <summary>
/// The client received a packet!
/// </summary>
/// <param name="Packet">The packet that the client received.</param>
private async Task Client_OnReceivedData(NetworkClient Sender, Packet P)
{
switch (P.ID)
{
case (byte)AuthPacketIDs.ServerInitialAuthResponse:
ServerInitialAuthResponse InitialAuthResponsePacket =
ZeroFormatterSerializer.Deserialize<ServerInitialAuthResponse>(P.Data);
await OnPacketReceived?.Invoke(InitialAuthResponsePacket, P.ID, Sender);
break;
case (byte)AuthPacketIDs.SAuthProof:
m_HasBeenAuthenticated = true;
AuthProof AuthProofPacket = ZeroFormatterSerializer.Deserialize<AuthProof>(P.Data);
await OnPacketReceived?.Invoke(AuthProofPacket, P.ID, Sender);
break;
}
}
}
}
ServerNetworkManager:
using Parlo;
using Parlo.Packets;
using SecureRemotePassword;
using System.Collections.Concurrent;
using System.Net;
using ZeroFormatter;
namespace ParloProtocol
{
public class ServerNetworkManager
{
private static readonly Lazy<ServerNetworkManager> m_Instance = new(() => new ServerNetworkManager());
private Listener m_Listener = default!;
private SrpServer m_SRPServer = new SrpServer();
private SrpEphemeral m_ServerEphemeral = new SrpEphemeral();
private Task m_NetworkingTask = default!;
private ConcurrentDictionary<Guid, NetworkClient> m_Clients = new ConcurrentDictionary<Guid, NetworkClient>();
/// <summary>
/// Event invoked when a network error occurred.
/// </summary>
public event NetworkErrorDelegate OnNetworkError = default!;
/// <summary>
/// Gets an instance of this ServerNetworkManager.
/// </summary>
public static ServerNetworkManager Instance { get { return m_Instance.Value; } }
/// <summary>
/// Event invoked when a client received a packet.
/// </summary>
public event OnPacketReceivedDelegate OnPacketReceived = default!;
public async Task Listen(string IP, int Port)
{
await using Listener m_Listener = new Listener(new ParloSocket(false));
m_Listener.OnConnected += M_Listener_OnConnected;
m_Listener.OnDisconnected += M_Listener_OnDisconnected;
IPEndPoint Endpoint = new IPEndPoint(IPAddress.Parse(IP), Port);
PacketHandlers.Register((byte)AuthPacketIDs.ClientInitialAuth, false,
new OnPacketReceived(Client_OnReceivedData));
PacketHandlers.Register((byte)AuthPacketIDs.CAuthProof, false,
new OnPacketReceived(Client_OnReceivedData));
await m_Listener.InitializeAsync(Endpoint);
}
/// <summary>
/// A new client connected!
/// </summary>
/// <param name="Client">The client that connected.</param>
private async Task M_Listener_OnConnected(NetworkClient Client)
{
await Task.Run(() =>
{
Guid NewClientGuid = Guid.NewGuid();
//False means the key already existed which should be impossible.
if (!m_Clients.TryAdd(NewClientGuid, Client))
throw new NetworkException("ServerNetworkManager: Key already existed!");
Client.OnReceivedData += Client_OnReceivedData;
});
}
/// <summary>
/// Received a packet from a client.
/// </summary>
/// <param name="Packet">The packet that was received.</param>
private async Task Client_OnReceivedData(NetworkClient Sender, Packet P)
{
//PacketStream PStream = (PacketStream)Packet;
switch (P.ID)
{
case (byte)AuthPacketIDs.ClientSignup:
ClientSignup SignUpPacket = ZeroFormatterSerializer.Deserialize<ClientSignup>(P.Data);
await OnPacketReceived?.Invoke(SignUpPacket, P.ID, Sender);
break;
case (byte)AuthPacketIDs.ClientInitialAuth:
ClientInitialAuth InitialAuthPacket = ZeroFormatterSerializer.Deserialize<ClientInitialAuth>(P.Data);
await OnPacketReceived?.Invoke(InitialAuthPacket, P.ID, Sender);
break;
case (byte)AuthPacketIDs.CAuthProof:
AuthProof AuthProofPacket = ZeroFormatterSerializer.Deserialize<AuthProof>(P.Data);
await OnPacketReceived?.Invoke(AuthProofPacket, P.ID, Sender);
break;
}
}
/// <summary>
/// A client disconnected!
/// </summary>
/// <param name="Client">The client that disconnected.</param>
private async Task M_Listener_OnDisconnected(NetworkClient Client)
{
}
}
}
NetworkException:
namespace ParloProtocol
{
/// <summary>
/// Thrown when something network-related goes awry!
/// </summary>
internal class NetworkException : Exception
{
/// <summary>
/// Creates a NetworkException.
/// </summary>
/// <param name="Message">The message that goes with the exception.</param>
public NetworkException(string Message) : base(Message)
{
}
}
}