/SimpleRemoteMethods

A project that allows to easily create a secure client-server part of project.

Primary LanguageC#Apache License 2.0Apache-2.0

SimpleRemoteMethods

A project that allows to easily create a secure and cross-platform client-server part of project. Based on protobuf serialization and HttpListener.

Start server

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.

Contracts

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.

Client class generation

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

Prepare Windows for server

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.

Authentication

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

Token distribution

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

Bruteforce checker

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

Events

/// <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;

MaxConcurrentCalls

If concurrent calls count exceeds the value of the MaxConcurrentCalls property, server suspends receiving all incoming connections.

server.MaxConcurrentCalls = 20; // Default value

MaxMessageLength

Maximum length of request in bytes.

server.MaxMessageLength = 20000; // Default value

Current request context

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

Exceptions handling

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.