Upload
api-26304929
View
216
Download
1
Embed Size (px)
DESCRIPTION
Working with Custom sinks in Remoting
Citation preview
.NET Remoting Customization Made Easy: Custom Sinks
Introduction
Isn’t .NET development easy? I, for one, believe that our life as developers is simpler with the .NET
framework comparing to prior technologies such as COM / DCOM under C++. .NET Remoting is,
without a doubt, an excellent example for this simplicity. After reading a comprehensive article or
two chapters of your favorite remoting book, you can immediately begin creating powerful
distributed applications. .NET Remoting also offers outstanding flexibility and various
customization options. However, this is where simplicity ends. In my opinion, .NET Remoting is
very easy to use but not all that easy to customize. To do so, you must be familiar with the inner
plumbing of the .NET Remoting infrastructure and are required to write numerous lines of code
you couldn’t care less about. Don’t get me wrong. I do believe that a more aware developer, one
that understands the underlying development infrastructure, is essentially a better developer. I
just don’t think that it means understanding and implementing every little detail as .NET Remoting
customizations sometimes require. In this article I would like to present a small library, which
simplifies one the most important aspects of the .NET Remoting customization: Custom Sinks.
What are Sinks?
When you work with a remote object, you do not hold a reference to that object, but a reference
to a proxy. The proxy is an object that looks and feels exactly like the remote object and can
convert your stack based method calls into messages, and send them to the remote object. In
order to send a message to the remote object, the proxy uses a chain of sinks. It calls the first sink
in the chain and provides it with the message. The sink optionally modifies the message, and
passes it to the next sink, and so on. One of the sinks along the way is a formatter sink. The task
of this special sink is to serialize the message into a stream. Sinks after the formatter sink operate
on the stream, because at this point the message is no longer relevant (and is provided to the
sinks as information only). The last sink in the stream is the transport sink, which is in charge of
sending the data to the server and waiting for a response. When response arrives, the transport
sink returns it to the previous sink and the response starts finding its way back to the proxy. Along
the way, the response goes through the formatter sink, which deserializes the response back into
a response message.
What happens on the server side? You guessed right. The server also holds a chain of sinks. This
time the chain leads to the target object. The first sink is the transport sink. Along the way lies the
formatter sink and finally there is the stack builder which does exactly the opposite of the client-
side proxy. It converts the message into a stack based method call to the target object. When the
target object’s method returns, information (return value, ref parameters, etc) is packed into a
message, which is returned back through the same sink chain starting with the stack builder and
ending with the transport sink.
.NET Remoting Customization Made Easy: Custom Sinks
In reality there are more sinks than the ones in the above figure, but I wanted to keep things
simple and chose to show only the ones relevant to the current discussion. Otherwise I would miss
the entire point of this article, wouldn’t I?
Custom Sinks
As shown in the above figure, it is possible to add custom sinks to the chain, both on the client
side and server side. So when do we decide to develop our own custom sink? We usually do it
when we want to inspect or modify the data sent from the proxy to the remote object and / or the
data returned from the remote object back to the proxy.
Let’s say that you want to encrypt the information sent over the wire between the client and the
server. To do so, you can create a client-side custom sink that encrypts outgoing request data and
decrypts incoming response data. You should also create a server-side custom sink that decrypts
request data arriving from the client and encrypts response data sent back to the client.
Custom sinks can be placed either before or after the formatter sink, depending on whether they
are designed to manipulate the message or the serialized stream. The encryption custom sink
would want to work on the stream (it doesn’t care about the logical meaning of the message; it
just needs to scramble it). Therefore the client-side custom sink should be situated after the
.NET Remoting Customization Made Easy: Custom Sinks
formatter sink (after the message is serialized to a stream) whilst the server-side custom sink
should be situated before the formatter sink (before the stream is desterilized back to a message).
As another example, let’s say you want the client to send username and password information
and the server, which should examine them before allowing access to the target object. You could
add the username and password as parameters to every method of the target object. This would
effectively add the required information to the message, but would be rather cumbersome. As an
alternative, you can create a client-side custom sink that adds the username and password to
every outgoing message and a server-side custom sink that retrieves this information and throws
an exception (which will be propagated back to the client) if the username and / or password are
invalid. Since these custom sinks work on the message, rather than on the serialized stream, the
client-side custom sink should be placed before the formatter sink (before the message is
serialized to a stream) and the server-side custom sink should be placed after the formatter sink
(after the stream is desterilized back to a message).
Cool! How do I do it?
Here is the catch. In order to implement your own custom sink, you will work hard! You must
define a class that implements at least one of the IMessageSink, IClientChannelSink and
IServerChannelSink interfaces, depending on whether you define a client-side or server-side
custom sink and whether your sink will be situated before or after the formatter sink. You should
make sure to forward every call to the next sink. You must implement different logic for
synchronous and asynchronous calls. Furthermore, the way asynchronous calls are handled is a
completely different story for each of the above three interfaces. Wait! There is more. As a dessert
you should also define a Sink Provider class that implements another interface
(IClientChannelSinkProvider or IServerChannelSinkProvider) and is able to create instances
of your custom sink upon request from the .NET Remoting infrastructure.
These tasks are surely doable, but are quite tedious, time-consuming and error-prone. When I first
realized all I had to do here, I asked myself – couldn’t they provide a base class that takes care of
all of these details? Can’t I handle only my own business logic by implementing only the relevant
parts of the custom sink? Well, I didn’t find such a class in the class library, so I decided to write
one on my own. Admittedly, this class does not cover every custom sink scenario. By simplifying
things, you sometimes loose some flexibility. However, I believe that the class is valid for most
real-world custom sink scenarios. The class BaseCustomSink and friends are contained in the
CustomSink class library. You can download the library as well as sample derived custom sinks
and sample client and server with full source code.
Features
.NET Remoting Customization Made Easy: Custom Sinks
The BaseCustomSink class, as its name implies, is a base class for custom sinks.
Main features:
Supports client-side and server-side sinks.
Supports synchronous and asynchronous calls.
Can be safely used in multithreaded applications.
Automatic reading of data from configuration file.
Allows derived classes to decide on runtime whether or not they want to be added to the
sink chain.
Calls a static initialization method of the derived class, if present.
These features will be further described and demonstrated in the following sections.
Basic Custom Sinks
In order to demonstrate the use of the CustomSink library, let’s implement encryption client /
server sinks. The source code for these sinks can be found in the accompanying SampleSinks
class library. Since I would like to concentrate in implementing the sinks, rather than delve into
cryptography, I will show the implementation of LameEncpriptionClientSink and
LameEncryptionServerSink, which simply add / subtract a delta from every byte in the stream,
as a simulation of encryption. In the future I may publish real-world encryption sinks, based on the
library presented here.
Before implementing the sinks, let’s create a helper class LameEncryptionHelper that will handle
the actual encryption. It will have a constructor that accepts the delta value, and two methods:
Encrypt and Decrypt. The Encrypt method looks as follows:
public Stream Encrypt(Stream source)
{
byte tempByteData;
int tempIntData;
MemoryStream encrypted = new MemoryStream();
while ((tempIntData = source.ReadByte()) != -1)
{
tempByteData = (byte)tempIntData;
tempByteData += this.delta;
encrypted.WriteByte(tempByteData);
}
encrypted.Position = 0;
return encrypted;
}
.NET Remoting Customization Made Easy: Custom Sinks
The Decrypt method is almost identical, with one difference – it simply subtracts the delta
instead of adding it.
The BaseCustomSink class contains two virtual methods, ProcessRequest and
ProcessResponse. The ProcessRequest method is called when request data is on its way to the
target object and the ProcessResponse method is called when the response data is on its way
back to the proxy. You may override these methods in order to add your own processing.
protected virtual void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
protected virtual void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
As you can see, the parameter list of both methods is almost identical.
message – this is the message being transferred.
headers – created by the formatter sink and allows adding logical information after the
message has been serialized.
stream – the stream that contains the serialized message. We can modify the stream or
assign a new stream.
state – in the ProcessRequest we may assign the state to any object that we later need in
the ProcessResponse.
Examine the implementation of these methods for the LameEncryptionClientSink:
protected override void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
{
stream = this.encryptionHelper.Encrypt(stream);
headers["LamelyEncrypted"] = "Yes";
}
.NET Remoting Customization Made Easy: Custom Sinks
protected override void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
{
if (headers["LamelyEncrypted"] != null)
{
stream = this.encryptionHelper.Decrypt(stream);
}
}
Both methods use a member field of the type LameEncryptionHelper in order to perform the
actual encryption. The ProcessRequest also adds information to the headers, specifying that the
stream was lamely encrypted. Here is the implementation for the LameEncryptionServerSink:
protected override void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
{
if (headers["LamelyEncrypted"] != null)
{
stream = this.encryptionHelper.Decrypt(stream);
state = true;
}
}
protected override void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
{
if (state != null)
{
stream = this.encryptionHelper.Encrypt(stream);
headers["LamelyEncrypted"] = "Yes";
}
}
.NET Remoting Customization Made Easy: Custom Sinks
The code for the server sink is designed to be capable of working with any client, regardless of
whether they use the LameEncryptionClientSink. The ProcessRequest method therefore
examines the headers to determine whether the stream was lamely encrypted. In such case the
method performs two things: it decrypts the method and assigns true to the state, in order to
signal the ProcessResponse to encrypt the response. This way, only clients that sent encrypted
request streams will receive encrypted response streams. The ProcessResponse method
examines the state object to see whether it was assigned (the actual value doesn’t matter since it
can only be true or null), and if it was, encrypts the response stream.
That’s it! Our custom sinks are now ready to be used. I will demonstrate how the client and server
utilize the sinks using configuration files. Before I do that, I must say a word about providers.
The .NET Remoting infrastructure does not directly create custom sinks. Instead it creates a sink
provider class, which is able to create the custom sinks upon demand. The CustomSinks class
library contains two providers: CustomClientSinkProvider and CustomServerSinkProvider,
which are able to provide BaseCustomSink derived classes. You don’t need to worry about these
classes. Simply specify the appropriate one (client or server) in the configuration file.
filename: Basic_SampleClient.exe.config
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http">
<clientProviders>
<formatter ref="soap" />
<provider
type="CustomSinks.CustomClientSinkProvider, CustomSinks"
customSinkType="LameEncryption.LameEncryptionClientSink, SampleSinks" />
</clientProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
filename: Basic_SampleServer.exe.config
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" port="7878">
.NET Remoting Customization Made Easy: Custom Sinks
<serverProviders>
<provider
type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.LameEncryptionServerSink, SampleSinks" />
<formatter ref="soap" />
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
As shown above, the provider is given the custom sink type using the customSinkType attribute.
The type is specified in the format “namespace.class, assembly”. In my sample, the custom sink
classes reside in a namespace named LameEncryption in an assembly named SampleSinks. The
client and server applications will invoke RemotingConfiguration.Configure and pass the
configuration file name as a parameter. Note that the sinks appear after the formatter in client
configuration file, and before the formatter in the server configuration file. This is crucial for the
sinks to function properly, since they must operate on the stream.
NOTE: I used an HTTP channel in my example, but you can easily switch to TCP.
You may now run the provided sample client and server application to see the custom sinks in
action. Actually you won’t see much, as the encryption will be done unnoticeably (after all, that’s
the whole point). However, I added some console outputs, which will prove that the sinks actually
work...
In this section, I demonstrated the relative ease of creating simple custom sinks using the
CustomSinks library. After having simplified things, I feel like complicating them a bit... For many
custom sinks, the basic features described in this section will suffice. However, to avoid resorting
to ‘manual’ implementation of custom sinks when we need just a little bit more functionality, I
added some more advanced features into the CustomSink library. You may stop here if that’s all
you need and refer to this article in the future.
In the following sections I will further develop the Lame Encryption sinks, to demonstrate
additional features of the CustomSinks library. Since I want to leave the simplest sample intact,
any further developments will be incorporated into EnhancedLameEncryptionServerSink and
EnhancedLameEncryptionClientSink (as if the original names weren’t long enough...). If you
wish to test the enhanced sinks, make sure to open SampleClient.cs and SampleServer.cs and
uncomment the relevant lines.
Accessing Configuration Data
.NET Remoting Customization Made Easy: Custom Sinks
To design more general custom sinks, we may sometimes need to access data from a
configuration file. For example our Lame Encryption sinks may want to read the delta value (the
value added / subtracted to / from every byte in the stream). This can be easily accomplished
when deriving a sink from BaseCustomSink. If the configuration file contains a customData
element under the provider element, the custom sink will be able to retrieve it through its
constructor. Let’s review the following excerpt of the modified client and server configuration files
(for the sake of brevity, I present here only the clientProviders and serverProviders
elements):
Enhanced_SampleClient.exe.config
<clientProviders>
<formatter ref="soap" />
<provider type="CustomSinks.CustomClientSinkProvider, CustomSinks"
customSinkType="LameEncryption.EnhancedLameEncryptionClientSink, SampleSinks">
<customData delta = "15" />
</provider>
</clientProviders>
Enhanced_SampleServer.exe.config
<serverProviders>
<provider type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.EnhancedLameEncryptionServerSink, SampleSinks">
<customData delta = "15" />
</provider>
<formatter ref="soap" />
</serverProviders>
In order to retrieve this data, the sink should have a constructor that accepts one parameter of
the type SinkCreationData. In such case, this constructor will be called and be provided with the
customData element through this parameter. Note that given the presence of such constructor,
the parameterless constructor will never be called (even if there is no customData element for the
appropriate sink).
Let’s review the constructor of the modified custom client sink
(EnhancedLameClientEncryptionSink). The server sink’s constructor is identical.
public EnhancedLameEncryptionClientSink(SinkCreationData creationData)
{
byte delta = 1;
if (creationData.ConfigurationData.Properties["delta"] != null)
{
delta = byte.Parse(
.NET Remoting Customization Made Easy: Custom Sinks
creationData.ConfigurationData.Properties["delta"].ToString());
}
this.encryptionHelper = new LameEncryptionHelper(delta);
}
Self Exclusion from Sink Chain
One of the main differences between client and server sinks is the timing of their creation. Server
sinks are created once, when the channel is configured. Client sinks are created every time a new
proxy is created (every proxy may have a different chain of sinks). Thus, client sinks may be
created multiple times.
Your custom sink (either client-side or server-side) can prevent its addition to the sink chain in
runtime. If for some reason (after inspecting the customData for an instance), your custom sink
decides that it shouldn’t be a part of the chain after all, it may throw an ExcludeMeException
from its constructor. The provider will catch this exception and act accordingly. Furthermore, if
your custom sink decides that it should never be created again, it can be more specific. Instead
of throwing the ExcludeMeException every time its constructor is called, it can provide true to
the constructor of ExcludeMeException to signal that it wants to be excluded permanently. After
that, the provider will not even attempt to create an instance of the provider.
Notes:
1. The excludeMePermanently (the parameter of the ExcludeMeException’s constructor) is
relevant only to client-side sinks. It is ignored when thrown by server-side sinks.
2. The excludeMePermanently works on a per provider basis. If the same client-side sink
appears multiple times in the configuration file, for example in both HTPP and TCP
channels, and the custom sink’s constructor for the HTTP channel throws and exception
specifying it should be excluded permanently, it will only be excluded for the HTTP
channel. The provider will still attempt to create instances of this custom sink for the TCP
channel.
Obtaining Additional Sink Creation Parameters
Your custom sinks may obtain additional information upon their creation. This is done by having a
constructor that accepts one parameter of type ClientSinkCreationData or
ServerSinkCreationData (depending on the type of your custom sink). Both derive from
SinkCreationData, which I presented in the previous section. This constructor has precedence
over any other constructor. If it exists, it will be the only one to get called.
.NET Remoting Customization Made Easy: Custom Sinks
The ClientSinkCreationData class contains the following fields: ConfigurationData – the
previously discussed SinkProviderData object, channel – the channel for which the sink is
created, Url – the URL of the remote object, and RemoteChannelData – data about the channel at
the server side (when applicable). You may refer to the parameters of
IClientChannelSinkProvider.CreateSink in the .NET Framework SDK documentation for
further details.
The ClientSinkCreationData class contains the following fields: ConfigurationData – the
previously discussed SinkProviderData object, and channel – the channel for which the sink is
created. You may refer to the parameters of IServerChannelSinkProvider.CreateSink in
the .NET Framework SDK documentation for further details.
Back to our Lame Encryption example, let’s say that we don’t need encryption when the target
object resides on “localhost”. Review the following constructor (which supports previously
developed functionality as well). The new constructor for EnhancedLameClientEncryptionSink is
a follows:
public EnhancedLameEncryptionClientSink(ClientSinkCreationData creationData)
: this((SinkCreationData)creationData)
{
Uri uri = new Uri(creationData.Url);
if (uri.IsLoopback)
{
throw new ExcludeMeException();
}
}
Notes:
Since the EnhancedLameEncryptionServerSink (and the basic
LameEncryptionServerSink for that matter) was already designed to support both
encrypted and unencrypted communication, it does not require any modifications.
The first constructor, the one that takes SinkCreationData as a parameter, is now
redundant (not listed here, but still present in the code). However I left it because it is
presented in previous sections. Since I already kept it, I redirected to it instead of writing
again the code for retrieving the delta from configuration file. However, for efficiency
reasons, you should normally first decide whether to throw an ExcludeMeException and
only afterwards perform additional construction logic.
Static Initialization
.NET Remoting Customization Made Easy: Custom Sinks
Since client-side custom sinks may be created numerous times, you may sometimes want your
class to initialize as much static information as possible. This way, you refrain from processing the
same information for every newly created instance. Normally, when you need static initialization,
you add a static constructor to your class. You may certainly adopt this approach for your custom
sink. However it has one major drawback. If your static constructor throws an exception, this
exception cannot normally be caught and handled.
Here is my solution. Define the following method in your custom client-side sink:
public static void Init(SinkProviderData data, ref object perProviderState)
{ }
This method is guaranteed to be called before any instance of your custom sink is created. You
can now place the call to RemotingConfiguration.Configure in try / catch block and catch any
exception this method might throw. Moreover, this method is provided with the data from the
customData element in the configuration file. NOTE: although this method is more relevant to
client-side sinks, it is valid in server-side sinks as well.
Important: There is a major distinction between this method and a static constructor. If your
custom sink appears more than once in the configuration file, the Init method will be called
multiple times, once per occurrence. In such scenarios, you should avoid assigning data to static
fields, because every call to Init will override the fields values assigned in previous calls to. To
overcome this problem, use the perProviderState.
The perProviderState allows you to save data on a per provider basis. If your sink appears
multiple times in the configuration file, the .NET Remoting infrastructure will create multiple
instances of CustomClientSinkProvider, one for each occurrence. You can take advantage of
this behavior by assigning one data object of your choice to the perProviderState parameter of
the Init method. This way, your object will be held within the provider object and multiple calls to
Init (which are done from different instances of the provider) will not override the previously set
data. Your custom sink will be able to access the data through the inherited
BaseCustomSink.PerProviderData property.
The Init method is not demonstrated in the Lame Encryption sample. However the SampleSinks
project contains another example, The Credentials Sinks, which uses this feature. The Credentials
Sinks are described below.
A Deeper Look at ProcessRequest and ProcessResponse
.NET Remoting Customization Made Easy: Custom Sinks
The first three parameters of ProcessRequest and ProcessResponse behave differently
depending on whether your custom sink is a server-side or client-side sink and whether it is
situated before or after the formatter sink. Here is the complete analysis.
ProcessRequest
Client-side, before the formatter sink:
message – request message.
headers – null.
stream – null. Do not assign this parameter to another stream.
Client-side, after the formatter sink:
message – request stream message. Do not modify the message as it has already been
serialized.
headers – request transport headers.
stream – request stream.
Server-side, before the formatter sink:
message – null.
headers – request transport headers.
stream – request stream.
Server-side, after the formatter sink:
message – request message.
headers – request transport headers.
stream – null. Do not assign this parameter to another stream.
ProcessReponse
Client-side, before the formatter sink:
message – response message.
headers – null.
stream – null. Do not assign this parameter to another stream.
Client-side, after the formatter sink:
message – null.
headers – response transport headers.
.NET Remoting Customization Made Easy: Custom Sinks
stream – response stream.
Server-side, before the formatter sink:
message – response message. Do not modify the message as it has already been
serialized.
headers – response transport headers.
stream – response stream.
Server-side, after the formatter sink:
message – response message.
headers – null.
stream – null. Do not assign this parameter to another stream.
The Credentials Sinks
The Lame Encryption sinks presented throughout this article manipulate the stream of the request
and the response. For completeness, I also included the Credential Sinks, which demonstrate
message-based processing.
The task of the client-side sink is to add the username and password to every outgoing
communication. The server-side sink verifies these credentials and throws an exception if they are
found to be invalid. The server-side retrieves the credentials from the configuration file. The
client-side sink also retrieves the credentials from the configuration file. However, different
credentials can be assigned to different servers and even ports.
You are invited to review the DemoCredentialServerSink and DemoCredentialClientSink in
the SampleSinks project.
How to Build Custom Sink Providers for .NET Remoting
The remoting framework in .NET contains a feature set designed to support intra-process communications and
application integration functions; however, for many applications the default remoting behaviors may not deliver
all the features that you need. For example, what if you want to implement an encryption scheme that secures
the messaging layer for your remoting application? To do so, you can make both the client and the server
.NET Remoting Customization Made Easy: Custom Sinks
application aware of the encryption and decryption requirements for the messages, but that will require some
redundant code for both client and server that you can't easily leverage across applications. In contrast, using
the extensibility features built into the .NET remoting framework lets you customize the remoting processes to
suit your application's needs. This article shows you how to build a custom message serialization sink for a
sample remoting application that captures SOAP messages and serializes data to the remoting client's file
system.
Understanding .NET Remoting Infrastructure
To understand how to extend.NET remoting using custom sinks, you must first understand a little about how the
remoting infrastructure works. The basic building blocks of the remoting architecture include:
Proxy objects used to forward calls to remote objects
Message objects used to carry the necessary data to invoke a remote method
Message sink objects used to process messages for remote method calls
Formatter objects used to serialize the message formats for transfer
Transport channel objects used to transfer the serialized messages from one process to another.
For the remoting extension example in this article, pay close attention to the message sink objects and the
formatters and transport channels, because they are key features in enabling the remoting infrastructure to
support method invocations. Each call to a remote object reference generates a message that serves as a
container for data exchanges. Message objects are dictionary objects encapsulated by the IMessage interface.
Each message involved in a remoting call passes through a chain of message sinks. Message sinks are objects
that cooperate to receive notifications for messages and process those messages. There are sinks on both the
client and server sides of a remoting call. Each message sink passes the message to the next sink in the chain
both before and after the message is transported.
Included in these message sinks are formatter sinks which serve to encode the data contained by the message
into a format suitable for transmission over the wire. By default, .NET remoting supports both a SOAP formatter
sink and a binary formatter sink, but you can extend the remoting infrastructure to support custom formatters.
Constructing a Remoting Application
You can implement three different interfaces for message sinks: IMessageSink, IClientChannelSink, and
IServerChannelSink. The difference between these interfaces is that the IMessageSink interface can access
and change the original dictionary object regardless of the serialization format, whereas the other two interfaces
have access to the serialized message represented as stream objects only. Another difference is that the
IClientChannelSink makes a distinction between synchronous and asynchronous calls during the request
processing phase; but IServerChannelSink does not make that distinction. The sample project demonstrates
how to implement a data serialization sink derived from IClientChannelSink.
To implement the ClientSinkSerializer, first create a simple remoting server with a single method exposed.
Here's an example.
.NET Remoting Customization Made Easy: Custom Sinks
public interface IRemotingExample
{ string GetHelloWorld(); }
public class RSExample : MarshalByRefObject, IRemotingExample
{
public string GetHelloWorld()
{
return "Hello World";
}
}
The RSExample class has a single GetHelloWorld method, which returns the string "Hello World". The sample
code defines the IRemotingExample interface in a separate assembly so it can be shared by the client and the
server. I'll use IIS to host the remoting server application. To configure the application, create a virtual directory
with a bin subdirectory, and copy the assemblies containing the remoting server class and the interface to that
bin directory. Then create a Web.config file, as shown in Listing 1, and place it in the virtual directory. The
configuration file lets you specify the type, assembly name, and URI for the remoting server.
Next you'll construct a client application to consume the remoting server. The sample uses a Windows forms
application with one form containing a Button and a Label control. Add the event-handler code shown in Listing
2 to handle the button's Click event. Finally, add the following line of code to the form's constructor:
RemotingConfiguration.Configure(filepath);
Calling the Configure method and passing a string parameter specifying the location of the client application's
configuration file lets you specify important details for the remoting infrastructure, such as the URI for the
remoting endpoint, the channel type (HTTP or TCP) to use for communications, and the formatter type to use for
message formatting. After constructing the client application, you should be able to run it and invoke the
remoting server's GetHelloWorld method.
You now have a working remoting application, and you're ready to add in the custom sink and sink provider to
save the incoming message contents using the remoting framework's extensibility.
Creating the Custom Sink Provider
To start, you'll need to build a class that can save message contents to a file using the serialized stream format
accessible through the implementation of the IClientChannelSink interface. The reason for using the
IClientChannelSink interface is that it provides the required functions and properties for client channel sinks, and
by implementing this interface you can customize your own sinks. This interface defines a ProcessMessage
method that you can use to extend the message sink and serialize the message contents to the file system (see
the ClientSinkSerializer class in Listing 3). To implement this custom sink, you need to create a sink provider
.NET Remoting Customization Made Easy: Custom Sinks
class that implements IClientSinkProvider. In the CreateSink member function of IClientSinkProvider, use the
following lines of code:
public IClientChannelSink CreateSink
(IChannelSender channel, string url, object remoteChannelData)
{
IClientChannelSink next = _nextProvider.CreateSink
(channel, url, remoteChannelData);
return new ClientSinkSerializer(next);
}
The preceding code returns a new ClientSinkSerializer class instance, passing
the next sink in the sink chain as a parameter to the constructor. That enables
the ClientSinkSerializer class to perform its processing and then pass the
message data onto the client's transport sink.
To make use of the client sink provider, you must configure the remoting client
application using a configuration file entry so the application can
programmatically register the message sink provider. The application uses the
channel definition parameters defined in the client application's configuration file
to create the sink on the client as soon as it registers the channel. Here's a
sample configuration file excerpt:
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http">
<clientProviders>
<formatter ref="soap" />
<provider type=
"SerializationSink.ClientSinkSerializerProvider,
SerializationSink" />
</clientProviders>
</channel>
</channels>
<client>
<wellknown type="RemotingServer.RSExample, RSExample"
url="http://localhost/RemotingExample/RSExample.soap" />
You can apply
serialization sink
techniques similar to
those shown in this
article to enable
message compression,
message level
encryption, message
logging, and other
useful functions.
.NET Remoting Customization Made Easy: Custom Sinks
</client>
</application>
</system.runtime.remoting>
</configuration>
The element contains the defined channel properties, including the definition of the element and its nested
element, which has attributes for the message sink provider types and assembly names. In addition, the element
defines the formatter type and channel type for communications, which are SOAP and Http respectively in this
example. When the client application is configured to use the sink provider, the message contents will be
serialized to a file prior to the invocation of the remoting server method. The data captured from the
responseStream when calling the GetHelloWorld method is the following:
<SOAP-ENV:Envelope
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0"
SOAP-ENV:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<i2:GetHelloWorld id="ref-1"
xmlns:i2=
"http://schemas.microsoft.com/clr/nsassem/RemotingInterface.
IRemotingExample/RemotingInterface">
</i2:GetHelloWorld>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The message is in a SOAP format—as expected based on the configuration of the client and the remoting
server. The body of the SOAP message specifies the remoting server method being invoked, GetHelloWorld.
The extensibility of the .NET remoting framework, including the use of custom sink providers for client
applications consuming remoting services, lets you leverage the base plumbing of the remoting infrastructure to
build custom applications, tweaking behavior as needed. You can use similar remoting extension techniques to
develop custom formatters and add customization to remoting transports, meaning you can take advantage of
different serialization formats and transport mechanisms for your remoting applications. You can apply
serialization sink techniques similar to those shown in this article to enable message compression, message
level encryption, message logging, and other useful functions. In addition, using the configuration support
.NET Remoting Customization Made Easy: Custom Sinks
provided for remoting applications lets you take advantage of the extensibility features and the "plug-in"
architecture long after developing and deploying the core application components.
Securing Remoting Calls and Validating Client
.NET Classes used :
System.Runtime.Remoting
System.Runtime.Remoting.Messages
System.Runtime.Remoting.Contexts
System.Reflection
.NET Remoting Customization Made Easy: Custom Sinks
Introduction
We are making a smart-client application, where the assemblies will be installed to a central, Remoting server. The client,
when it connects to the server, will download the assemblies that are not there already or those assemblies that have
been updated on the server. And we are making it like a framework and so authorized third-party vendors can also come
in to the picture. They can also make their own assemblies, which should be part of the server and then downloaded to
the client. Also, much application-specific information, including the SQL connection information, will be downloaded via
an XML “catalog”.
The Risk
And there comes a risk. The risk of “somebody else” (in other words, any “un-authorized application) trying to connect to
our Remoting server and downloads the information. Or it can be such that those “un-authorized” applications,
referencing our assemblies and then trying to download the information through the back-door entry. We needed a way to
prevent anything of this kind happening.
Thoughts on different security aspects
We needed a custom security mechanism to prevent this. Thinking about such a security mechanism and I already had to
take care of the following things in my mind.
I first thought of using the StrongNameIdentityPermission attribute, given by the .NET framework itself. But the
problem is that we are allowing third party vendors to develop around our framework and hence assemblies with different
strong-name public key need to call other assemblies. The StrongNameIdentityPermission attribute allows only strong
named assemblies with one public key to call the assembly in question.
There were already some Remoting objects created. Specifying a security mechanism should be made easily
applicable to those and of course to future objects.
There should be a centralized way to bring a security mechanism where the Remoting objects should not worry about
it, but a layer above it should. So that changes to those security features can be made easily (Remember that the security
feature is not complete at any point of time. It is of evolving in nature).
And this is how I decided to move on…
Now, it became clear to me that to secure the Remoting calls, we need to validate the client. There should be some
information to be sent from the client, on which the security mechanism has to validate it. And it should be foolproof too.
Because I am asking the client itself to tell the truth!! (And which era I am living in, meanwhile?). Also, not all methods of
all Remoting objects can worry about calling a method to validate the client (and my colleagues were not ready to change
all their code too, to be honest!!). After taking care (read “worrying”) of all these things, I arrived at this kind of a security
mechanism.
The Security Mechanism
The remoting client, for each call, will set the Stack Trace of the client into a Call Context Data. For more information of
what is Call Context Data and why it is used, please refer my earlier articles. The Stack Trace is the one, which the client
.NET Remoting Customization Made Easy: Custom Sinks
cannot alter (but can reduce the number of stack frames, which is again a potential risk. We will discuss this later in the
article).
The call context data will get passed to the server for every remoting call. The server, on the other side, will take the stack
trace from the call context data. Then, the server will take each assembly of the stack trace, check its public key and
verify the same with the server side assembly’s public key. If they are same, the call is allowed (meaning only our
assembly has made the remoting call). If the client assembly is not having a public key or it is different from the server
side assembly, we assume that something is wrong and we throw a Security Exception to the client. The same is the
case if the Stack Trace is not sent at all by the client.
Well, the approach is OK. But who will take care of the validation done on the server side? Will every method of every
Remoting object do that? Or is there a way to commonly tackle this?
The answer is Yes. The solution is to “Intercept” the Remoting calls that come to the server, before the actual method is
called on the Remoting object. We will validate the Stack Trace, decide whether to allow the method to be called or not. If
we do not want to, we throw a Security Exception at this point itself. The implementation is done by using
DynamicProperty and DynamicSink.
Intercepting Remoting Calls using Dynamic Property and Sink
Now, so much explanation have been done in the first half of this article, let’s jump into some technical stuff in the
second. If you have decided to go for a sleep by now, please give me a last chance and let me pace up this article by
showing the implementation stuff!!
Dynamic Property
The System.Runtime.Remoting.Contexts namespace has an interface called IDynamicProperty and
IContributeDynamicSink interfaces. While the first one has only one read-only property called Name , the second has also
only one method GetDyanmicSink, which returns an instance of IDynamicMessageSink. We will be creating a class that
implements both these interfaces. And this is the class that we are going to “Register” for interception. To sum up, our
class (called as DynamicProperty) will look like this:
Imports System.Runtime.Remoting.Contexts
Imports System.Runtime.Remoting.Messaging
Imports System.Reflection
Public Class DynamicProperty
Implements IDynamicProperty, IContributeDynamicSink
Private Const SINK_NAME As String = "MyDynamicSink"
Public ReadOnly Property Name() As String Implements
System.Runtime.Remoting.Contexts.IDynamicProperty.Name
Get
Return SINK_NAME
.NET Remoting Customization Made Easy: Custom Sinks
End Get
End Property
Public Function GetDynamicSink() As
System.Runtime.Remoting.Contexts.IDynamicMessageSink Implements
System.Runtime.Remoting.Contexts.IContributeDynamicSink.GetDynamicSink
Return New DynamicSink
End Function
End Class
Have a look at the GetDynamicSinkmethod. It returns an instance of a class called DynamicSink. But where is it? We will
create one now.
Dynamic Sink
The DynamicSink class implements an interface called IDynamicMessageSink. This has two methods -
ProcessMessageStart and ProcessMessageFinish. If you have guessed correctly, the first method will fire whenever a
Remoting method is going to get called. The next method will fire whenever a Remoting method is called and the call is
about to get returned to the client.
Naturally, we need to intercept the call before it is being actually called by the Remoting object itself. So, we will place our
code in the ProcessMessageStart method.
The DynamicSink class will look like this:
Public Class DynamicSink
Implements IDynamicMessageSink
Public Sub ProcessMessageFinish(ByVal replyMsg As
System.Runtime.Remoting.Messaging.IMessage, ByVal bCliSide As Boolean, ByVal bAsync As
Boolean) Implements
System.Runtime.Remoting.Contexts.IDynamicMessageSink.ProcessMessageFinish
Try
Catch ex As Exception
Throw ex
End Try
End Sub
Public Sub ProcessMessageStart(ByVal reqMsg As
System.Runtime.Remoting.Messaging.IMessage, ByVal bCliSide As Boolean, ByVal bAsync As
Boolean) Implements
System.Runtime.Remoting.Contexts.IDynamicMessageSink.ProcessMessageStart
Try
.NET Remoting Customization Made Easy: Custom Sinks
‘----------------- STEP 1 -------------------------------------------
Dim Props As ArrayList = CType(CType(CType(reqMsg.Properties,
System.Collections.IDictionary).Values, System.Collections.ICollection),
System.Collections.ArrayList)
‘----------------- STEP 2 -------------------------------------------
Dim i As Int32
For i = 0 To Props.Count - 1
If Not Props(i) Is Nothing AndAlso Props(i).GetType Is
GetType(LogicalCallContext) Then
Dim callcontext As LogicalCallContext = CType(Props(i),
LogicalCallContext)
If callcontext Is Nothing Then
Throw New Security.SecurityException
Else
‘----------------- STEP 3 -------------------------------------------
Dim ContextData As ICallContextData2 =
CType(callcontext.GetData(“UserInfo”), ICallContextData2)
If ContextData Is Nothing Then
Throw New Security.SecurityException
Else
‘----------------- STEP 4 -------------------------------------------
Dim secHlpr As New SecurityHelper
If Not secHlpr.IsAssemblyCorrect(ContextData.StackTrace) Then
Throw New Security.SecurityException
Else
Exit Sub
End If
End If
End If
End If
Next
Throw New Security.SecurityException
Catch ex As Exception
Throw ex
End Try
End Sub
End Class
I know I just can’t win your hearts if I don’t explain the things. Just have a look at the ProcessMessageStart method
carefully. For a better understanding, I have split up the logic into 4 steps.
.NET Remoting Customization Made Easy: Custom Sinks
Step 1:
The ProcessMessageStart method contains an IMessage component. This contains details about the current method call.
The IMessage interface has a property called Properties (IMessage.Properties), which is a Dictionary object. This
Dictonary object’s “Values” property is an ArrayList of different information. One of them (and presumably last of them) is
the LogicalCallContext.
Step 2:
So, we are just looping through the properties until we find the LogicalCallContext in the ArrayList. In case we are unable
to find one, we just throw a Security Exception from here, which prevents the actual method being called on the Remoting
object.
Step 3:
Once we find the LogicalCallContext, we are getting the actual data by using the LogicalCallContext.GetData() method.
We are just type casting to an instance of ICallContextData2 interface (which is created by us and that contains a
property called StackTrace, which contains the client Stack Trace. You can find this file in the downloaded zip file) In
case the GetData method returns Nothing, we are again throwing a Security Exception.
Step 4:
If we have found one, we are passing this stack trace to a method called IsAssemblyCorrect which loops through the
stack trace and check against the same assembly in the server side to see whether both have the same public keys are
not. This method exists in a class called SecurityHelper, designed primarily to take care of these things. You can find the
class in the downloaded zip file.
Register the Remoting object for Interception
Now we have our Interception classes ready, we need to make a small change in our Remoting object’s constructor for
this to work. In the constructor of the Remoting object, call the RegisterDynamicProperty method of the Context object.
Assume that you have a Remoting object called MyRemotingObject, you would write the following code in the
constructor:
Public Sub New()
Context.RegisterDynamicProperty(New DynamicProperty(),Me,Nothing)
End Sub
And this is all that you need to do for intercepting all the Remoting calls for your Remoting object, MyRemotingObject.
The Client Side Implementation
The client side implementation is very simple. Before initiating a call to the MyRemotingObject class, you need to set the
Call Context Data. Setting the Call Context Data is very simple. Here is how you should do:
MyObj = CType(Activator.GetObject(GetType(MyRemotingObject), remotingUrl),
MyRemotingObject)
.NET Remoting Customization Made Easy: Custom Sinks
Dim CallCtxt As ICallContextData2 = MyRemotingObject.CallContextData
‘This property will expose the ICallContextData2 interface from your ‘Remoting object.
CallCtxt.StackTrace = New StackTrace
‘Set the client’s Stack Trace to the property
CallContext.SetData(“UserInfo”,CallCtxt)
‘Name should be same as that of the server.
MyRemotingObject.HelloWorld()
‘Calling this method will pass on the stack trace to the server. You
‘need to have all the assemblies in the stack trace on the server side
‘also. This constraint itself prevents “un-authorized” or Non-Server
‘assemblies from calling your code!!
Stack Trace – A caution
If the client decides to shave off a part of the stack trace (by using the SkipFrames parameter of the StackTrace
constructor, this means that the stack trace may not have the “un-authorized” assembly’s information and therefore, the
server would validate fine. The Security Helper class will try to avoid checking for .NET related assemblies (like
System.Windows.Forms.dll etc). But what happens if the Stack Trace contains only .NET assemblies? Because the client
has “intelligently” taken some frames out, this is another potential risk. Therefore, the Security Helper class, which I have
given, will return true only if there is at least one assembly, which is other than the .NET, related assemblies.
Files For Download
This article accompanies some files put into a zip file. This zip file contains the following classes:
SecurityHelper – Which takes care of mapping client and server side assemblies and their public keys respectively.
CallContextData – Which Implements ICallContextData2 interface
ICallContextData2 – Interface, which has a property for getting the Stack Trace.
DynamicProperty – The one responsible for registering ContextBoundObjects.
DynamicSink – The one where we actually validate the stack Trace.