Implementing a Custom Channel Sink

Implementing a Custom Channel Sink

Request and response messages pass from one end of a sink chain to the other. During this traversal, sinks have the opportunity to do work based on the message information. By creating a custom sink, we can hook ourselves into the sink chain and gain access to every message before transmission and after receipt. But custom sinks aren’t limited to just manipulating messages. A custom sink could perform some action based on the contents of the message. Finally, custom sinks don’t have to be symmetric, so you’re not required to have a sink on both the server and client. The following real-world uses for custom sinks should help you gain an understanding of their power:

  • Encryption sink

    Would encrypt all messages that are transmitted between the sender and receiver. An encryption sink would require a sink at both the sender and receiver.

  • Logging sink

    Would write a message to a database each time an object is created or a method is called. The sink could be located on the server only, the client only, or both.

  • Access-time sink

    Would block calls on remote objects during certain periods. The sink could be located on the server only, client only, or both.

  • Authentication sink

    Would disallow certain users based on information contained in the message sent from the client. In this case, a client channel sink and server channel sink would exist, but they would perform different operations. The client would add authentication data to the message, whereas the server would take that information and determine whether the method can be called.

In the first part of this chapter, we created a transport-specific custom channel. In doing so, we had to create a custom sink for the client and server. In this section, we’ll create a custom sink that allows us to modify the behavior of a sink chain. When we created the server-side channel classes, we didn’t create a class that implemented IServerChannelSinkProvider. This was the only place where the server and client class weren’t symmetric. It wasn’t necessary for us to create a server sink provider because we were the first sink in the chain. To support placing a custom sink in the sink chain, we’ll implement IServerChannelSinkProvider. Table 7-10 contains the members of IserverChannel­SinkProvider.:

Table 7-10. Members of System.Runtime.Remoting. Channel.IServerChannelSinkProvider

Member

Member Type

Description

Next

Property

Allows you to set and get the next IServerChannelSinkProvider object

CreateSink

Method

Returns an IServerChannelSink for the beginning of a new sink chain

GetChannelData

Method

Returns the IChannelDataStore object for the current IServerChannelSinkProvider object

Creating the AccessTime Custom Sink

In the remainder of this chapter, we’ll create a custom sink that allows us to block method calls on remote objects during a particular period. The process will entail three steps:

  • Creating a sink named AccessTimeServerChannelSink that implements IServerChannelSink

  • Creating a provider named AccessTimeServerChannelSinkProvider that implements IServerChannelSinkProvider

  • Adding functionality for the custom implementation

The purpose of the AccessTime custom sink is to control the number of times method calls can be made on a remote object. During channel creation, you can set a starting and stopping time. If a method call is attempted between the starting and stopping time, this call won’t be processed—it will return an error.

AccessTime Projects

The sample code for the AccessTime custom sink has the following projects:
  • AccessTimeSinkLib

    Contains the implementation of the sink and the sink provider.

  • DemonstrationObjects

    Contains a class named Demo. Our example client and server will be remoting an instance of the Demo class.

  • Server

    Registers AccessTimeServerChannelSinkProvider and AccessTimeServerChannelSink with channel services. This project then simply waits for the user to terminate the server by pressing Enter.

  • Client

    Like the server project, this project registers AccessTimeServerChannelSinkProvider and AccessTimeServerChannelSink with channel services. The project then demonstrates a method call on the remote object.

Implementing the AccessTimeServerChannelSink Class

Let’s start by taking a high-level look at the public interface of AccessTimeServerChannelSink:

public class AccessTimeServerChannelSink : IServerChannelSink
{
    public AccessTimeServerChannelSink( IServerChannelSink nextSink, 
                                        String blockStartTime, 
                                        String blockStopTime,
                                        bool isHttpChannel )
    {
         
    }

    public ServerProcessing ProcessMessage( 
        IServerChannelSinkStack sinkStack,
        IMessage requestMsg,
        ITransportHeaders requestHeaders,
        Stream requestStream,
        out IMessage responseMsg,
        out ITransportHeaders responseHeaders,
        out Stream responseStream )
    {
         
    }

    public void AsyncProcessResponse( 
        IServerResponseChannelSinkStack sinkStack,
        Object state,
        IMessage msg,
        ITransportHeaders headers,
        Stream stream )
    {
    }

    public Stream GetResponseStream( 
        IServerResponseChannelSinkStack sinkStack,
        Object state,
        IMessage msg,
        ITransportHeaders headers )
    {
        return null;
    }

    public IServerChannelSink NextChannelSink
    {
        get
        {
            return m_NextSink;
        }
    }

    public IDictionary Properties
    {
        get
        {
            return null;
        }
    }
}

As you can see, the AccessTimeServerChannel sink is basic. Of the public members, the constructor and process message will perform the work. The constructor for AccessTimeServerChannel is as follows:

public AccessTimeServerChannelSink( IServerChannelSink nextSink, 
                                    String blockStartTime, 
                                    String blockStopTime,
                                    bool isHttpChannel )
{
    m_NextSink = nextSink;

    if(( blockStartTime.Length > 0 ) && ( blockStopTime.Length > 0))
    {
        ParseHourAndMinute( blockStartTime, 
                            out m_BlockStartTimeHour, 
                            out m_BlockStartTimeMinute );
        ParseHourAndMinute( blockStopTime, 
                            out m_BlockStopTimeHour, 
                            out m_BlockStopTimeMinute );
    }
    m_IsHttpChannel = isHttpChannel;
}

The two parameters, blockStartTime and blockStopTime, appear in the format HH:MM, where HH is the hour and MM is the minute. We’ll use the private method ParseHourAndMinute to populate our member time variables.

ProcessMessage is where we allow or deny method calls. If we aren’t in a blocked time period, we call ProcessMessage on the next sink. If we are in a blocked time period, we must return and let the client know we couldn’t fulfill its request. Here’s the implementation of ProcessMessage:

public ServerProcessing ProcessMessage( 
                               IServerChannelSinkStack sinkStack,
                               IMessage requestMsg,
                               ITransportHeaders requestHeaders,
                               Stream requestStream,
                               out IMessage responseMsg,   
                               out ITransportHeaders responseHeaders,
                               out Stream responseStream )
{
    // If we are not in a blocked time period, 
    // send the message down the chain.
    if( !IsBlockTimePeriod( ) )
    {
        return m_NextSink.ProcessMessage( sinkStack,
                                          requestMsg,
                                          requestHeaders,
                                          requestStream,
                                          out responseMsg,
                                          out responseHeaders,
                                          out responseStream );
    }
    else
    {
        if( m_IsHttpChannel )
        {
            responseMsg = null;
            responseStream = null;

            responseHeaders = new TransportHeaders();
            responseHeaders["__HttpStatusCode"] = "403";

            return ServerProcessing.Complete;
        }
        else
        {
            throw new RemotingException( "Attempt made to call a " +
                                    "method during a blocked time" );
        }
    }
}

First, notice that we call the private method IsBlockTimePeriod. IsBlockTimePeriod first checks to see whether we have nonzero time values in our member variables. If this condition is met, we compare the times and return true if we should block and false if the request message should be processed. If we aren’t blocking, we pass the same parameters to m_NextSink.ProcessMessage. When we do block, we respond to the client in one of two ways, depending on the type of channel. If the transport channel we’re using in this sink chain is HttpChannel, we’ll respond with an HTTP status code of 403. Upon receipt of this HTTP status code, the client will throw an exception that will bubble up to the user of the channel’s code. When the transport channel isn’t HttpChannel, we throw a RemotingException. The exception will be packaged into a response message and rethrown on the client side. But before any of this can happen, AccessTimeServerChannelSinkProvider must create an AccessTimeServerChannelSink object.

Implementing the AccessTimeServerChannelSinkProvider Class

The main responsibility of AccessTimeServerChannelSinkProvider is to inject our sink into the sink chain. The secondary responsibility is to collect the data needed by the sink. AccessTimeServerChannelSinkProvider implements the interface IServerChannelSinkProvider:

public class AccessTimeServerChannelSinkProvider : 
    IServerChannelSinkProvider
{
    private IServerChannelSinkProvider m_NextProvider;

    String m_BlockStartTime;
    String m_BlockStopTime;        

    public AccessTimeServerChannelSinkProvider( 
        IDictionary properties,
        ICollection providerData )
    {
        // Get the start and stop time.
        // An exception will be thrown if these keys are not found
        // in properties.
        m_BlockStartTime = ( String )properties["BlockStartTime"];
        m_BlockStopTime = ( String )properties["BlockStopTime"];
    }

    public IServerChannelSink CreateSink( IChannelReceiver channel )
    {
        bool IsHttpChannel = channel is HttpServerChannel;

        IServerChannelSink nextSink = null;
        if( m_NextProvider != null )
        {
            nextSink = m_NextProvider.CreateSink( channel );
        }

        return new AccessTimeServerChannelSink( nextSink, 
                                                m_BlockStartTime, 
                                                m_BlockStopTime, 
                                                IsHttpChannel );
    }

    public void GetChannelData( IChannelDataStore channelData )
    {
    }

    public IServerChannelSinkProvider Next 
    {
        get
        {
            return m_NextProvider;
        }
        set
        {
            m_NextProvider = value;
        }
    }
}

The most important method in AccessTimeServerChannelSinkProvider is CreateSink. CreateSink has three responsibilities:

  • Create the remainder of the sink chain.

  • Create AccessTimeServerChannelSink.

  • Test to see whether IChannelReceiver is an HttpServerChannel type.

To create the remainder of the sink chain, we call CreateSink on the next provider. The next provider is stored in the member m_NextProvider. This member is set by the .NET Remoting infrastructure by using the Next property. When the call to m_NextProvider.CreateSink returns, we’ll have a reference to the first sink in this chain. We then pass the reference to the constructor of AccessTimeServerChannelSink, so our sink will be part of the chain.

Adding AccessTimeServerChannelSink to a Configuration File

The following configuration file demonstrates how to add a sink:

<configuration>
   <system.runtime.remoting>
      <application>
         <service>
            <wellknown mode="Singleton" 
                       type="DemonstrationObjects.Demo,Œ 
                                    DemonstrationObjects" 
                       objectUri="DemoURI" />        
         </service>
         <channels>
            <channel ref="http" port="4000">
               <serverProviders>        
                  <provider type="AccessTimeSyncLib.Œ
                     AccessTimeServerChannelSinkProvider,Œ
                     AccessTimeSinkLib"
                     BlockStartTime="10:00" BlockStopTime="16:00"/> 
                  <formatter ref="soap"/>
               </serverProviders>
            </channel>
         </channels>
      </application>
   </system.runtime.remoting>
</configuration>

Notice that under the channel element, we added the element serverProviders. The serverProviders element contains two elements. The first is provider. The provider element is where our sink is added by using the type attribute. You’ll also see the two attributes that define the time to disallow method calls. The second element under serverProviders is formatter. Recall from the custom channel that the constructor for FileServerChannel added a formatter only when the provider parameter was null; therefore, we must specify a formatter when adding our sink.