A project that allows to easily create a secure and cross-platform client-server part of project. Based on protobuf serialization and HttpListener.
Create and start server:
var server = new Server<IContracts>(contractsObject, useHttps, serverPort, secretKey);
server.StartAsync();
- [IContracts] is contracts interface;
- [contractsObject] - a class object derived from a contract interface that contains target methods;
- [useHttps] - boolean, use https;
- [serverPort] - server port;
- [secretKey] - key for intermediate AES encryption.
Example of contracts interface:
public interface IContracts
{
[Remote]
void TestMethod1();
[Remote]
int TestMethod2(string str);
}
Contracts class example:
public class Contracts: IContracts
{
public void TestMethod1()
{
Console.WriteLine("TestMethod1");
}
public TestObjectClass TestMethod2(string str)
{
return new TestObjectClass();
}
}
[ProtoContract]
public class TestObjectClass
{
[ProtoMember(1)]
public int TestProperty1 { get; set; }
[ProtoMember(2)]
public string TestProperty2 { get; set; }
}
More information about protobuf serialization: mgravell/protobuf-net.
Use srmGen.exe to generate client class.
srmGen.exe {path to contracts interface library} {namespace and interface name of target contracts} {result class namespace} {result class name} {generated file path}
Example how it looks in Lazurite.MainDomain in build events:
srmGen.exe bin\$(ConfigurationName)\netstandard2.0\Lazurite.MainDomain.dll Lazurite.MainDomain.IServer Lazurite.MainDomain LazuriteClient ..\Lazurite.MainDomain\LazuriteClient.cs
Generated class example: LazuriteClient.cs
In Windows, you need to perform a lot of actions to start the server: add a rule for a firewall, reserve an address, bind a certificate to a port. SimpleRemoteMethods has a library to do this automatically. Add SimpleRemoteMethods.Utils.Windows.dll and run [PrepareHttpServer] or [PrepareHttpsServer] method after creating the server. Example:
var server = new Server<IContracts>(contractsObject, true, serverPort, secretKey);
ServerHelper.PrepareHttpsServer(server, certificateHash, appUniqueId);
server.StartAsync();
var server = new Server<IContracts>(contractsObject, false, serverPort, secretKey);
ServerHelper.PrepareHttpServer(server, appUniqueId);
server.StartAsync();
- [certificateHash] - string, hash of target https certificate.
- [appUniqueId] - string, custom id of current app, used for firewall rules naming.
Interface IAuthentication allows to override standard authentication stub.
public interface IAuthenticationValidator
{
bool Authenticate(string userName, string password);
}
public class MyCustomAuthentication
{
public bool Authenticate(string userName, string password)
{
return userName == "someuser" && password = "password1";
}
}
var server = new Server<IContracts>(contractsObject, useHttps, serverPort, secretKey);
server.AuthenticationValidator = new MyCustomAuthentication();
server.StartAsync();
Instead of passing user/password every time remote method called, the user token is transferred. Token distribution proceed automatically.
Change token lifetime example:
server.TokenDistributor.TokenLifetime = TimeSpan.FromHours(24);
Revoke user token (when password/login changed, etc):
server.TokenDistributor.RevokeToken("someuser");
To all other, there is possible to override standard token distributor by creating a class, derived from ITokenDistributor:
/// <summary>
/// Class that conains logic for user token distribution
/// </summary>
public interface ITokenDistributor
{
/// <summary>
/// Returns true if the token was created and it is still alive
/// </summary>
/// <param name="token"></param>
/// <param name="tokenInfo">Info about token (user name, etc)</param>
/// <returns></returns>
bool Authenticate(string token, out TokenInfo tokenInfo);
/// <summary>
/// Creates new token for user/ip
/// </summary>
/// <param name="userName"></param>
/// <param name="clientIp"></param>
/// <returns>token string</returns>
string RequestToken(string userName, string clientIp);
/// <summary>
/// Cancel user token
/// </summary>
/// <param name="userName"></param>
void RevokeToken(string userName);
/// <summary>
/// The time interval while the token is alive
/// </summary>
TimeSpan TokenLifetime { get; set; }
}
server.TokenDistributor = new MyCustomTokenDistributor();
Simple bruteforce checker already exists in project, but user can override it by creating a class, derived from IBruteforceChecker:
public interface IBruteforceChecker
{
/// <summary>
/// Check last login activity and decides whether
/// the user is trying to bruteforce a password
/// </summary>
/// <param name="loginString">Is client user name or ip</param>
/// <returns></returns>
bool CheckIsBruteforce(string loginString);
/// <summary>
/// Check the user or ip is in wait list
/// </summary>
/// <param name="loginString">Is client user name or ip</param>
/// <returns></returns>
bool IsWaitListContains(string loginString);
}
server.BruteforceCheckerByLogin = new MyCustomBruteforceChecker();
server.BruteforceCheckerByIpAddress = new MyCustomBruteforceChecker();
/// <summary>
/// Raises when need to write log
/// </summary>
public event EventHandler<LogRecordEventArgs> LogRecord;
/// <summary>
/// Raises before server start
/// </summary>
public event EventHandler<EventArgs> BeforeServerStart;
/// <summary>
/// Raises after server started
/// </summary>
public event EventHandler<EventArgs> AfterServerStarted;
/// <summary>
/// Raises after server stopped
/// </summary>
public event EventHandler<EventArgs> AfterServerStopped;
/// <summary>
/// Access to http listener before server starts listen
/// </summary>
public event EventHandler<TaggedEventArgs<HttpListener>> HttpListenerCustomSetting;
/// <summary>
/// Access to http context on client connect
/// </summary>
public event EventHandler<TaggedEventArgs<HttpListenerContext>> HttpRequestCustomHandling;
/// <summary>
/// Access to request on client connect
/// </summary>
public event EventHandler<TaggedEventArgs<Request>> UserRequest;
/// <summary>
/// Access to response on user request
/// </summary>
public event EventHandler<TaggedEventArgs<Response>> ServerResponse;
/// <summary>
/// Access to user token request
/// </summary>
public event EventHandler<TaggedEventArgs<UserTokenRequest>> UserTokenRequest;
/// <summary>
/// Access to user token response
/// </summary>
public event EventHandler<TaggedEventArgs<UserTokenResponse>> ServerUserTokenResponse;
/// <summary>
/// Access to error response
/// </summary>
public event EventHandler<TaggedEventArgs<ErrorResponse>> ErrorServerResponse;
/// <summary>
/// Raises before method calls
/// </summary>
public event EventHandler<RequestEventArgs> MethodCall;
If concurrent calls count exceeds the value of the MaxConcurrentCalls property, server suspends receiving all incoming connections.
server.MaxConcurrentCalls = 20; // Default value
Maximum length of request in bytes.
server.MaxMessageLength = 20000; // Default value
Current request context allows you to determine current caller, user-ip and other useful information about execution of current method.
Example:
public interface IContracts
{
[Remote]
void TestMethod1();
[Remote]
int TestMethod2(string str);
}
public class Contracts: IContracts
{
private void WriteUserInfo()
{
var currentUser = Server<IContracts>.CurrentRequestContext.UserName;
var currentUserIp = Server<IContracts>.CurrentRequestContext.ClientIp;
Console.WriteLine(currentUser + " " + currentUserIp);
}
public void TestMethod1()
{
WriteUserInfo();
}
public int TestMethod2(string str)
{
WriteUserInfo();
return str.Length;
}
}
There is one class to determine which exception was thrown on the server: RemoteException.
try
{
await client.TestMethod1()
}
catch (RemoteException e) when (e.Code == ErrorCode.LoginOrPasswordInvalid)
{
// Do something when user/password is invalid
}
catch (RemoteException e) when (e.Code == ErrorCode.UnknownData || e.Code == ErrorCode.DecryptionErrorCode)
{
// Do something when secret key is invalid
}
catch (RemoteException e) when (e.Code == ErrorCode.BruteforceSuspicion)
{
// Do something when there is bruteforce suspition
}
catch (RemoteException)
{
// Other actions
}
catch (Exception)
{
// Other actions
}
All error codes: ErrorCode.