Uniquely identifying clients using CallContext [Gagan Gupta]

Here is a remoting sample that demonstrates a way to communicate IP address from client to service (it can be easily tweaked to do vice-versa).

Sample uses CallContext [1] to achieve this communication. CallContext provides ability to pass data from one call to another and "flow" the data in the background as opposed to explicitly passing it.

For this sample I have a simple CalculatorService that has just one method Add(). Here is the implementation of the service

        public int Add(int a, int b)

        {

            string clientAddress = CallContext.GetData("ClientAddress").ToString();

            return (a + b);

        }

        static void Main(string[] args)

        {

            HttpChannel channel = new HttpChannel(8080);

            ChannelServices.RegisterChannel(channel);

            RemotingConfiguration.RegisterWellKnownServiceType(

                Type.GetType("CallContextSample.CalculatorService"),

                "ICalculator",

                WellKnownObjectMode.SingleCall

            );

            Console.ReadLine();

        }

The client side code is as follows:

        static void Main(string[] args)

        {

            HttpChannel channel = new HttpChannel();

            ChannelServices.RegisterChannel(channel);

            ICalculator calc = (ICalculator)Activator.GetObject(typeof(ICalculator), "localhost:8080/ICalculator");

           

            // get client's IP address and put it in CallContext

            IPHostEntry ipEntry = Dns.GetHostByName(Dns.GetHostName());

            IPAddress[] clientAddress = ipEntry.AddressList;

            // This sets a CallContextDataSlot on this thread.

            // CallContextString will convert the ThreadLocalDataStore into

            // a property on the message, which will then be merged into the

            // ThreadLocalDataStore on the server. This is a nice way to pass

            // information from client to server threads and back again.

            // NOTE: This data store has logical thread scope. If an

            // application domain has more than one thread making calls,

            // each thread will have its own logical CallContext, and the

            // server thread will keep each separate.

            CallContext.SetData("ClientAddress", new CallContextIPAddress(clientAddress[0].ToString()));

            Console.WriteLine("Result from service: " + calc.Add(2, 5));

        }

Here is the implementation of the CallContextIPAddress object that is inserted in the CallContext

    // One method of communicating between client and server is

    // to use the CallContext. Calling CallContext essentially puts the data

    // in a Thread Local Store. This means that the information is available

    // to that thread or that "logical" thread (across application domains) only.

    [Serializable]

    public class CallContextIPAddress : ILogicalThreadAffinative

    {

        String ipAddress = "";

        public CallContextIPAddress(String str)

        {

            ipAddress = str;

        }

        public override String ToString()

        {

            return ipAddress;

        }

    }

Here is a WCF [2] (AKA Remoting 3.0) sample.

Sample uses extensibility point IClientMessageInspector to insert IP address in each request sent to the service.

namespace MessageInspectorSample

{

    class StringConstants

    {

        public const string ClientAddress = "clientaddress";

        public const string NS = "ns";

        public const string BaseAddress = "localhost:8000/";

        public const string EndpointPath = "CalculatorService/";

    }

    [ServiceContract]

    public interface ICalculator

    {

        [OperationContract]

        int Add(int a, int b);

    }

    [ServiceBehavior]

    public class CalculatorService : ICalculator

    {

        public int Add(int a, int b)

        {

            // get the client's IPAddress from the headers of incoming message

            string clientAddress = OperationContext.Current.IncomingMessageHeaders.GetHeader<string>(StringConstants.ClientAddress, StringConstants.NS);

            return (a + b);

        }

    }

    class Program

    {

        static void Main(string[] args)

        {

            ServiceHost service = new ServiceHost(typeof(CalculatorService), new Uri(StringConstants.BaseAddress));

            WSHttpBinding binding = new WSHttpBinding();

            service.AddServiceEndpoint(typeof(ICalculator), binding, StringConstants.EndpointPath);

            service.Open();

            // Client

            ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>(binding, new EndpointAddress(StringConstants.BaseAddress + StringConstants.EndpointPath));

           

            // get client's IP address

            IPHostEntry ipEntry = Dns.GetHostEntry(Dns.GetHostName());

            IPAddress[] clientAddress = ipEntry.AddressList;

           

            // add MyMsgInspector behavior which in turn adds itself to MsgInspectors

            factory.Endpoint.Behaviors.Add(new MyMsgInspector(clientAddress[0].ToString()));

            ICalculator proxy = factory.CreateChannel();

            try

            {

                Console.WriteLine("Client: Result from service: " + proxy.Add(2, 5));

            }

            catch (Exception e)

            {

                Console.WriteLine(e);

            }

            ((IChannel)proxy).Close();

            factory.Close();

            Console.ReadLine();

            service.Close();

        }

    }

    // IClientMessageInspector is an extensibility point for inspecting and possibly transforming all

    // messages that flow through the proxy

    class MyMsgInspector : IClientMessageInspector, IEndpointBehavior

    {

        string clientIPAddress;

        #region IClientMessageInspector members

        public MyMsgInspector(string str)

        {

            clientIPAddress = str;

        }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)

        {

            // here we add the client's IPAddress as a header to the outgoing request

            // benefit of adding the IPAddress here is that it gets added to all the requests

            // If you are concerned about performance and need to add IPAddress

            // for only certain requests then you can use OperationContext to do the same

            MessageHeader mh = MessageHeader.CreateHeader(StringConstants.ClientAddress, StringConstants.NS, clientIPAddress);

            request.Headers.Add(mh);

            return null;

        }

        public void AfterReceiveReply(ref Message reply, object correlationState) { }

        #endregion

        #region IEndpointBehavior Members

        void IEndpointBehavior.Validate(ServiceEndpoint serviceEndpoint) { }

        void IEndpointBehavior.AddBindingParameters(ServiceEndpoint serviceEndpoint, BindingParameterCollection bindingParameters) { }

        void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior)

        {

            behavior.MessageInspectors.Add(this);

        }

        void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, EndpointDispatcher endpointDispatcher) { }

        #endregion

    }

}

[1] msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconremotingexamplecallcontext.asp

[2] wcf.netfx3.com

Note:

-The samples above are not exhaustively tested and are not supported by Microsoft.

-.Net Remoting (and this sample) does not do authentication or encryption by default. Therefore, it is recommended that you take all necessary steps to make certain of the identity of clients or servers to prevent security attacks like ‘man in the middle spoofing’.