Freigeben über


SSL Offloader Using WCF 4.0 Routing Service

WCF Routing Service is a new feature introduced with WCF 4.0. While this article is not about explaining what it is (complete details about WCF Routing Service is available here), I must admit that this new feature is simply awesome. It has made pain points like protocol bridging and handling communication exceptions a no brainer for any developer. Just get your configurations correct, everything else will be taken care of by the routing service. Through this example, I have covered the following features of routing service:

                -      Content Based Routing

          - Error Handling

          - Protocol Bridging

           - Custom Filters

A SSL Offloader is an intermediary device which accepts an incoming HTTPS request, offloads SSL and forwards HTTP traffic to backend servers. Certain SSL Offloaders even do the work of authenticating a client. With the work of authentication delegated away, service performance improves. In this example, SSL Offloader does the following work:

                1. Receives a HTTPS request from a client.

                2. Authenticates the client.

                3. Offloads SSL and forwards HTTP request to backend available service.

                4. Load balances incoming requests in round robin manner between 2 backend services.

Now let’s get into implementation specifics. In this example, we have 2 backend services: Primary Calculator Service and Secondary Calculator Service. Contracts of both these services are identical to enable round robin load balancing of requests in between these services. Following are their bindings and endpoints:

Service Binding

      <basicHttpBinding>

        <binding>

          <security mode="None" />

        </binding>

      </basicHttpBinding>

Primary Service Endpoint

        <host>

          <baseAddresses>

            <add baseAddress="https://localhost:8080/PrimaryCalculatorService"/>

          </baseAddresses>

        </host>

        <endpoint address=""

                  binding="basicHttpBinding"

                  contract="ICalculatorContract" />

Endpoint of secondary service is identical, only hosted on port 8081. Note the missing bindingConfiguration property in the endpoint definition. This is a new feature of WCF 4.0. As long as there is a single binding definition of a given type (in this example basicHttpBinding), there is NO need to link that binding to an endpoint. That binding is considered as the standard binding. Hence no name property associated with the binding definition either. If you want to define another binding (say this time with security enabled), you need to give it a name and associate it with a endpoint using bindingConfiguration. There is one another cool feature of WCF 4.0 which I will like to mention here: Standard Endpoints. More details are available here. I have used it to define my mex endpoint.

Standard MEX Endpoint

        <endpoint isSystemEndpoint="true"

                  kind="mexEndpoint"

                  address="mex" />

Before I get into the SSL Offloader, let’s take a look at the client application first. Remember that a client needs to connect over HTTPS and pass some authentication tokens. In this example, we will use UsernameToken. Following is the client binding:

      <customBinding>

        <binding>

          <security authenticationMode="UserNameOverTransport"

                    includeTimestamp="false"

                    requireDerivedKeys="false" />

          <httpsTransport />

        </binding>

      </customBinding>

Now let’s get into SSL Offloader implementation. To reiterate the requirements here:

                                              1. Communication in between the client and SSL Offloader is over HTTPS.

                                              2. Communication in between the SSL Offloader and calculator service is over HTTP.

                                              3. Client authentication is done by the SSL Offloader.

Keeping these in mind, following is the offloader binding:

      <wsHttpBinding>

        <binding>

          <security mode="Transport">

            <transport clientCredentialType="None" />

          </security>

        </binding>

      </wsHttpBinding>

Why is the clientCredentialType set to ‘none’? That’s where the custom filter comes into picture. It not only routes an incoming request in a round robin fashion, but also authenticates the client. Hence there is no need to implement a custom UserNamePasswordValidator. Note the seamless protocol bridging which happens here. Routing infrastructure takes care of routing a request from a HTTPS protocol to a HTTP protocol without a single line of application code being written.

Other implementation details (SSL Offloader and client endpoints) are usual. Let’s get to the routing table implementation. This is how the routing table looks like:

    <routing>

      <filters>

        <filter name="primaryFilter" filterType="Custom"

        customType="SSLOffloaderCustomFilter, SSLOffloaderUsingCustomFilterService"

        filterData="group1"/>

        <filter name="secondaryFilter" filterType="Custom"

           customType="SSLOffloaderCustomFilter,SSLOffloaderUsingCustomFilterService"

           filterData="group1" />

      </filters>

      <filterTables>

        <filterTable name="primaryRoutingTable">

          <add filterName="primaryFilter" endpointName="primaryCalc"

               backupList="primaryBackUp"/>

          <add filterName="secondaryFilter" endpointName="secondaryCalc"

               backupList="secondaryBackUp"/>

        </filterTable>

      </filterTables>

      <backupLists>

        <backupList name="primaryBackUp">

          <add endpointName="secondaryCalc"/>

        </backupList>

        <backupList name="secondaryBackUp">

          <add endpointName="primaryCalc"/>

        </backupList>                   

      </backupLists>     

    </routing>

In addition to the custom filter, note the 2 backupList. It implements exception handling by routing a request to backup endpoint when primary endpoint is unavailable. Simple, isn’t it? To know more about error handling by routing service, refer here.

Now let’s move to custom filter implementation details. Since I will not get into the basics of implementing a custom filter, I will recommend everyone to review ‘Advanced Filters’ sample available with WCF 4.0 samples. 2 important works are performed by this custom filter:

Client Authentication

What we do here is read the security header and validate the username and password tokens.

private bool ValidateClientCredentials(Message requestMessage)

{

       MemoryStream stream = new MemoryStream();

       MessageBuffer buffer = requestMessage.CreateBufferedCopy(Int32.MaxValue);

       buffer.WriteMessage(stream);

       stream.Position = 0;

       byte[] requestByte = stream.ToArray();

      string requestStr = System.Text.Encoding.UTF8.GetString(requestByte);

      XmlDocument doc = new XmlDocument();

       doc.LoadXml(requestStr);

      XmlNodeList securityNode = doc.GetElementsByTagName("o:Security");

       if (securityNode.Count == 0)

              throw new InvalidOperationException("No security header is available in the incoming request");

XmlNodeList usernameNode = doc.GetElementsByTagName("o:Username");

       XmlNodeList passwordNode = doc.GetElementsByTagName("o:Password");           

       if (usernameNode.Count == 0 || passwordNode.Count == 0)

       {

              throw new InvalidOperationException("No username or password present in the incoming request");

       }

       else if( string.IsNullOrEmpty(usernameNode[0].InnerText) || string.IsNullOrEmpty(passwordNode[0].InnerText))

       {

              throw new ArgumentNullException("Username / Password cannot be null or empty");

       }

       else if(usernameNode[0].InnerText.Equals("Microsoft") && passwordNode[0].InnerText.Equals("Microsoft"))

       {

        return true;

       }               

       return false;

}

This method is called from all implementations of IMessageFilterTable<FilterData>.GetMatchingXXX methods. One such implementation is as follows: 

public bool GetMatchingFilter(System.ServiceModel.Channels.MessageBuffer messageBuffer, out MessageFilter filter)

{

       filter = null;

       if(ValidateClientCredentials(messageBuffer.CreateMessage()))

       {

              filter = GetNextAvailableFilter();

       }

       return (filter!= null);

}

Since we are operating on the message body, we need to set the following highlighted property to false inside service behaviors:

<routing filterTableName="primaryRoutingTable" routeOnHeadersOnly="false"/>

This makes sure that only those variants of GetMatchingXXX are called by runtime whose 1st parameter is a MessageBuffer type. This ensures that we do not directly operate on an incoming Message object since a Message object is a read – once type.

Round Robin Load Balancing

This is implemented inside the following private method inside filter table inner class:

List<SSLOffloaderCustomFilter> filters = new List<SSLOffloaderCustomFilter>();

int filterIndex = 0;

private SSLOffloaderCustomFilter GetNextAvailableFilter()

{

       SSLOffloaderCustomFilter returnedFilter = null;

       int currentIndex = filterIndex;

      for (int i = currentIndex + 1; i < filters.Count; i++)

       {

       returnedFilter = filters[i];

       filterIndex = i;

       }

       if (returnedFilter == null)

       {

       for (int i = 0; i < currentIndex + 1; i++)

              {

              returnedFilter = filters[i];

                     filterIndex = i;

                     break;

              }

       }

       return returnedFilter;

}

As the name of the method indicates, all I am doing here is to choose one of the filters in a round robin fashion. With this I have covered the important implementation details which went behind developing this SSL Offloader example. Hope this demonstrates how awesome WCF 4.0 Routing Service is.

Comments

  • Anonymous
    May 10, 2011
    Is there a full working sample source solution available please?