theRainbird/CoreRemoting

Objects created by proxy call must be different, not singleton

Closed this issue · 6 comments

Describe the bug
When a proxy returns an object SubTypeService which created by ctor on server side the object instance always be the same.

To Reproduce
Steps to reproduce the behavior:

  • Run this code on net framework
using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
using CoreRemoting;
using CoreRemoting.DependencyInjection;
using CoreRemoting.Serialization.Binary;

namespace ProgramTest
{
    public class Program
    {
        private static void Main(string[] args)
        {
            try
            {
                //RegisterChannel(7801);
                //RegisterSingletonSAOType(typeof(SayHelloService));
                //var proxy = CreateTypeInstance<ISayHelloService>("localhost",
                //    7801);
                //var subTypeName1 = proxy.NewSubTypeService(true).GetName();
                //var subTypeName2 = proxy.NewSubTypeService(false).GetName();
                //Debug.Assert(subTypeName1 != subTypeName2);


                var server = new RemotingServer(new ServerConfig()
                {
                    HostName = "localhost",
                    NetworkPort = 7801,
                    RegisterServicesAction = container =>
                    {
                        // Make SayHelloService class available for RPC calls from clients
                        container.RegisterService<ISayHelloService, SayHelloService>(ServiceLifetime.Singleton);
                    },
                    Serializer = new BinarySerializerAdapter()
                });
                server.Error += Server_Error;
                server.Start();

                var client = new RemotingClient(new ClientConfig
                {
                    ServerHostName = "localhost",
                    ServerPort = 7801,
                    Serializer = new BinarySerializerAdapter()
                });

                client.Connect();

                // Create a proxy of the remote service, which behaves almost like a regular local object
                var proxy = client.CreateProxy<ISayHelloService>();
                var subTypeName1 = proxy.NewSubTypeService(true).GetName();
                var subTypeName2 = proxy.NewSubTypeService(false).GetName();
                Debug.Assert(subTypeName1 != subTypeName2);

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        private static void Server_Error(object sender, Exception e)
        {
            Console.WriteLine(e);
        }

        public static void RegisterChannel(int channelPort)
        {
            BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
            provider.TypeFilterLevel = TypeFilterLevel.Full;
            IDictionary props = new Hashtable();
            props["port"] = channelPort;
            props["name"] = "My-TcpChanel";
            props["machineName"] = Environment.MachineName;
            ChannelServices.RegisterChannel(new TcpChannel(props, new BinaryClientFormatterSinkProvider(), provider),
                false);
        }

        public static void RegisterSingletonSAOType(Type type)
        {
            RemotingConfiguration.RegisterWellKnownServiceType(type, "MyRemoteHost/I" + type.Name,
                WellKnownObjectMode.Singleton);
        }

        public static T CreateTypeInstance<T>(string computer, int port)
        {
            return (T)Activator.GetObject(typeof(T),
                "tcp://" + computer + ":" + port + "/MyRemoteHost/" + typeof(T).Name);
        }
    }

    
    public class SayHelloService : MarshalByRefObject, ISayHelloService
    {
        public ISubTypeService NewSubTypeService(bool isSystem)
        {
            return new SubTypeService(isSystem);
        }
    }

    public class SubTypeService : MarshalByRefObject, ISubTypeService
    {
        private string _name;
        public SubTypeService(bool isSystem)
        {
            if (isSystem)
                _name = "System";
        }

        /// <inheritdoc />
        public string GetName()
        {
            return _name;
        }
    }

    
    public interface ISayHelloService
    {
        ISubTypeService NewSubTypeService(bool isSystem);
    }

    [ReturnAsProxy]
    public interface ISubTypeService
    {
        string GetName();
    }

}

Expected behavior
SubTypeService objects must be different.

Additional notes
In commented code you can check that classic Remoting works as expected. Version of CoreRemoting is 1.1.14

CoreRemoting depends on a centralized service registry.
A service have to be registered, in order to be called from clients.
[ReturnAsProxy] does this registration implicitly, if the type is not already a registered service.
If the type is already registered, the registered version is returned.
Registering a new version on every call, would result in a memory leak.

CoreRemoting doesn't implement the concept of leases and sponsors.

CoreRemoting is thinking in well known services.
Classic .NET Remoting is thinking in objects.

Some .NET Remoting design patterns like proxy factory doesn't fit well for CoreRemoting.

I have removed the implicit service registration when returning a [ReturnAsProxy] decorated object with b664adf.

So any service must be registered, before it can be returned as proxy.
When registering the SubTypeService with SingleCall lifetime, then for every call on the returned proxy, a new object instance is created.

Important to understand is, that the instance created in NewSubTypeService method is never used. Service instance creation is done when the RPC message from a client is processed. inside RemotingSession.
This makes passing the isSystem parameter useless.

A possible workaround could be, returning a CoreRemoting.Serialization.ServiceReference object in the NewSubTypeService method. Within this, you could specify a service name.
Example:

                var server = new RemotingServer(new ServerConfig()
                {
                    HostName = "localhost",
                    NetworkPort = 7801,
                    RegisterServicesAction = container =>
                    {                        
                        container.RegisterService<ISayHelloService, SayHelloService>(ServiceLifetime.Singleton);
                        
                        container.RegisterService<ISubTypeService>(
                            factoryDelegate: () => new SubTypeService(isSystem: true),
                            lifetime: ServiceLifetime.Singleton, 
                            serviceName: "SystemSubTypeService");

                        container.RegisterService<ISubTypeService>(
                            factoryDelegate: () => new SubTypeService(isSystem: false),
                            lifetime: ServiceLifetime.Singleton, 
                            serviceName: "RegularSubTypeService");
                    },
                    Serializer = new BinarySerializerAdapter()
                });

The SayHelloService then must be modified as follows:

    public class SayHelloService : ISayHelloService // MarshalByRefObj is not needed for CoreRemoting
    {
        public CoreRemoting.Serialization.ServiceReference NewSubTypeService(bool isSystem)
        {
            return 
                new CoreRemoting.Serialization.ServiceReference(
                    serviceInterfaceTypeName: typeof(ISubTypeService).AssemblyQualifiedName,
                    serviceName: isSystem ? "SystemSubTypeService" : "RegularSubTypeService");
        }
    }

On the client side, the ServiceReference could be converted into a proxy:

var proxy = client.CreateProxy<ISayHelloService>();
var subType1Proxy = client.CreateProxy(proxy.NewSubTypeService(true));
var subType2Proxy = client.CreateProxy(proxy.NewSubTypeService(false));

var subTypeName1 = subType1Proxy.GetName();
var subTypeName2 = subType2Proxy.GetName();
Debug.Assert(subTypeName1 != subTypeName2);

The new overload of RemotingClient.CreateProxy is available in the latest commit (b62f177).

[ReturnAsProxy] attribute is not needed anymore, when this approach is used.

Sorry, this was my bad example.
I need the following:
Let's replace the proxy creation code with

var proxy = client.CreateProxy<ISayHelloService>();
proxy.NewSubTypeService("1").SayHello();
proxy.NewSubTypeService("2").SayHello();

And replace the description of the service with

    public class SayHelloService : ISayHelloService
    {
        public ISubTypeService NewSubTypeService(string login)
        {
            return new SubTypeService(login);
        }
    }

    public class SubTypeService : ISubTypeService
    {
        private string _userLogin;
        //The dictionary can be replaced by a database with logins and names
        private static Dictionary<string, string> _loginsAndNames = new Dictionary<string, string>()
            { { "1", "Kate" }, { "2", "Mike" } };
        public SubTypeService(string login)
        {
            _userlogin = login;
        }

        /// <inheritdoc />
        public void SayHello()
        {
            Console.WriteLine("Hello from user {0}", _loginsAndNames[_userLogin]);
        }
    }

    
    public interface ISayHelloService
    {
        ISubTypeService NewSubTypeService(string login);
    }

    [ReturnAsProxy]
    public interface ISubTypeService
    {
        void SayHello();
    }

I need to record every user action(here this action is SayHello) in the SubTypeService on behalf of the user who created it. And also close the proxy SubTypeService on the server side for a specific login without closing the rest of the SubTypeServices(#33). The client does not need to know anything about the real usernames, he only knows the login. Based on the passed login, the server knows the username and writes to the console who exactly called SayHello. This way each SubTypeService instance is bound to only one login

CoreRemoting has builtin support for user authentication. You can create your own authentication provider to implement your authentication logic (in most cases there is a database with user records and password hashes).

Just implement IAuthenticationProvider interface and register your authentication provider when creating ServerConfig. The Authenticatemethod accepts an array with login credentials (a credential consist of a name and a value). It must return true, if a user could be successfully authenticated using the provided credentials. The resolved identity of the user is passed back as an out argument.

Example:

public class ChatAuthProvider : IAuthenticationProvider
{
    private static Dictionary<string, string> _loginsAndNames = 
        new Dictionary<string, string>() { { "1", "Kate" }, { "2", "Mike" } };

    public bool Authenticate(Credential[] credentials, out RemotingIdentity authenticatedIdentity)
    {
        authenticatedIdentity = null;

        if (credentials == null)
            return false;

        var loginIDCredential = credentials.FirstOrDefault(c => c.Name == "login");

        // In a real application a database or LDAP directory will be queried here
        if (_loginsAndNames.ContainsKey(loginIDCredential.Value))
        {
            // If loginID is valid, create a RemotingIdentity for the resolved user
            authenticatedIdentity =
                new RemotingIdentity()
                {
                    Name = _loginsAndNames[loginIDCredential.Value],
                    IsAuthenticated = true,
                    Roles = new string[] { "ChatUser" } // Roles a optional, but may useful for authorization
                };

            return true;
        }
        
        return false;
    }
}

On server side, the custom authentication provider must be registered.

var serverConfig =
    new ServerConfig()
    {
        NetworkPort = 8080,
        AuthenticationRequired = true,
        AuthenticationProvider = new ChatAuthProvider()
    };

var server = new RemotingServer(serverConfig);

// Attach the authenticated user identity to the processing worker thread, before any call
server.BeforeCall += (sender, context) =>
{
    var session = context.Session;
    
    if (session.IsAuthenticated)
    {
        var identity = session.Identity;
        Thread.CurrentPrincipal =
            new GenericPrincipal(identity, identity.Roles);
    }
};

server.Start();

On client side, the credentials must be set in ClientConfig before Connect is called.

using var client = 
    new RemotingClient(new ClientConfig()
    {
        ServerPort = 8080,
        Credentials = new []
        {
            new Credential() { Name = "login", Value = "1" },            
        }
    });

client.Connect();

if (client.Identity.IsAuthenticated)
    Console.WriteLine($"Successfully logged in as '{client.Identity.Name}'.);

Inside your services on server side, you could query the identity of the calling client from the current worker thread.

    public class SubTypeService : ISubTypeService
    {
        public void SayHello()
        {
            var identity = Thread.CurrentPrincipal.Identity; // <-- Query identity of calling client user

            Console.WriteLine("Hello from user {0}", identity.Name);
        }
    }

This is the recommended solution for authenticating users.

Actually I don't need to register services with different serviceName. I just gave an unfortunate example of using a proxy created from another proxy to implement authentication.
In my application, authentication is implemented like this: there is a singleton AppServer object. It creates a Session object in case of successful authentication.

    public class AppServer : IAppServer
    {
        public ISubTypeService NewSession(string login, stgring password)
        {
            // Any check for login and password
            if (!_loginsAndNames.ContainsKey(login))
                throw new Exception("incorrect login");
            return new Session(login, password);
        }
    }

From which you can get server data

    public class Session : ISession
    {
        private string _userLogin;
        //The dictionary can be replaced by a database with logins and names
        private static Dictionary<string, string> _loginsAndNames = new Dictionary<string, string>()
            { { "1", "Kate" }, { "2", "Mike" } };
        public Session(string login, stgring password)
        {
            _userlogin = login;
        }

        public string GetData()
        {
            Console.WriteLine("User {0} gets data", _loginsAndNames[_userLogin]);
            //query to database
            return _db.GetData();
        }
    }

Getting data

    var proxy = CreateTypeInstance<IAppServer>("localhost", 7801);
    var session = proxy.NewSession("1", "password");
    var data = session.GetData();

The essence of authentication was either to create a Session object or not to create it. And access to the data maked through Session. Now with the implementation of IAuthenticationProvider, the need for a Session is eliminated

With the latest commit my code will look like this:

    var proxy = CreateTypeInstance<IAppServer>("localhost", 7801);
    var session = client.CreateProxy(proxy.NewSession("1", "password"));
    var data = session.GetData();

public class AppServer : IAppServer
    {
        public CoreRemoting.Serialization.ServiceReference NewSession(string login, stgring password)
        {

In my opinion, the solution with ReturnAsProxy was more convenient and looked more like classic Remoting. Less code changed compared to classic Remoting. Could you bring back the old ReturnAsProxy behavior?

Now I understand the concept of IoC and proxy registration and modified initial code like this

                    RegisterServicesAction = container =>
                    {
                        // Make SayHelloService class available for RPC calls from clients
                        container.RegisterService<ISayHelloService, SayHelloService>(ServiceLifetime.Singleton);
                        container.RegisterService<ISubTypeService>(() => new SubTypeService(SayHelloService.Instance), ServiceLifetime.Singleton);
                    },