Archive for the ‘c#’ Category

Grabbing the message body with Exchange Transport Agent

June 24, 2009

Exchange Server 2007 came with a new transport architecture, which you can so joyfully admire on http://technet.microsoft.com/en-us/library/aa996349.aspx.

I had this project where we needed to exchange emails between 2 networks with a different classification, and of course SMTP was one of the protocols that was not allowed to pass the bridge between both networks.

In fact, the only thing that was allowed was xml and this would be enforced with a xml firewall which would validate the xml packets against a set of predefined schema’s.

So, for us to be able to get the message through, we would have to intercept all email traffic, filter them -based on recipients- and serialize the outbound messages to send them through the xml gateway.

A possible solution was writing a Transport Agent that could be plugged into the Exchange Server, and that’s exactly what we did.
Intercepting the messages and route them, based on recipient was easy. There are numerous documents online that show you how to write a Transport Agent that logs all messages, or modifies the recipient list, …

In fact, the hardest part was grabbing the message body, simply because it was always formatted in unicode/utf16, regardless of our efforts to save it in utf8. A lot of older email clients don’t support the display of message bodies in unicode/utf16 and typically you would always see just the first character of the body followed by some weird character.

The solution to this problem lies in the fact that the message body, accessible through the MessageEventArgs e.MailItem.Message..Body.GetContentReadStream() is encoded in unicode/utf16. This should not be a problem if it could be directly streamed to a file with proper encoding defined.

Unfortunately there is no direct way to do this, so we ended up with adding a StreamReader to it, permitting us to read the stream line by line and writing it to a StreamWriter attached to a FileStream.

Even though we specified the StreamWriter to use Encoding.UTF8, somehow the file got always populated with what seemed like Unicode/UTF16. Our mistake was connecting the StreamReader with default arguments to the e.MailItem.Message..Body.GetContentReadStream(). In C#, by default a StreamReader uses utf8 encoding, where our Body was encoded in unicode/utf16. By telling the streamReader tu use Encoding.Unicode (UTF16) we were finally able to properly convert the message body to utf8 and save it to our xml file.

Here’s some code snippet:


StreamReader reader = new StreamReader(e.MailItem.Message.Body.GetContentReadStream(),System .Text.Encoding.Unicode, true);
FileStream fso = new FileStream(workingDir + @"\" + MessageGuid + ".body.processing", FileMode.OpenOrCreate, FileAccess.ReadWrite);
StreamWriter writer = new StreamWriter(fso, Encoding.UTF8);
string temp;
while ((temp = reader.ReadLine()) != null)
{
 writer.WriteLine(temp);
}
writer.Flush();
writer.Close();

fso.Close();
reader.Close();

Kerberos delegation and Service Identity in WCF

May 30, 2008

Scope:

Step-by-step walkthrough to establish a secure connection between a WCF Client and Service by using a Kerberos token. This example will focus on message security, though WCF also offers transport security. We will establish mutual authentication between service and client, using the wsHttpBinding.

Kerberos delegation will only have to be granted to the Service, not to the Client. We can choose to run the Service under a NetworkService or Localsystem ‘service’ account, or under a ‘domain’ account (domain\user). The Service will be self-hosted (serviceHost).

Requirements:

.NET 3.5

.Windows Domain with Active Directory

.Domain Administrator access
Tools:

SetSPN.exe, Klist.exe, Kerbtray.exe

SetSPN.exe makes it easy to define service principal names for computers or user accounts in the active directory. The other 2 tools can be useful to inspect the kerberos tickets on the machine where you will run the WCF Service. They can be downloaded from the microsoft site, in one of their Resource toolkits for windows servers

Running the Service under a Service Account

1. The Service

Code:

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceProcess;
using System.ServiceModel.Description;

namespace WCFWithKerberos
{
    class Program
    {
        static void Main(string[] args)
        {
            // create the service host
            ServiceHost myServiceHost = new ServiceHost(typeof(Echo));

            // create the binding
            WSHttpBinding binding = new WSHttpBinding();
            binding.Security.Mode = SecurityMode.Message;
            binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;

            // disable credential negotiation and establishment of the security context
            binding.Security.Message.NegotiateServiceCredential = false;
            binding.Security.Message.EstablishSecurityContext = false;

            // Creata a URI for the endpoint address
            Uri httpUri = new Uri("http://yamata:8000/Echo");

            // Create the Endpoint Address with the SPN for the Identity
            EndpointAddress ea = new EndpointAddress(httpUri, 
                              EndpointIdentity.CreateSpnIdentity("HOST/yamata.marbie.net:8000"));
            /* works with
             *  setspn -a HOST/yamata.marbie.net:8000 yamata
             */

            // Get the contract from the IEcho interface
            ContractDescription contract = ContractDescription.GetContract(typeof(IEcho));

            // Create a new Service Endpoint
            ServiceEndpoint se = new ServiceEndpoint(contract, binding, ea);

            // Add the Service Endpoint to the service
            myServiceHost.Description.Endpoints.Add(se);

            // Open the service
            myServiceHost.Open();
            Console.WriteLine("Listening...");
            Console.ReadLine();

            // Close the service
            myServiceHost.Close();
        }
    }

    public class Echo : IEcho
    {
        public string Reply(string message)
        {
            Console.WriteLine(message);
            return message;
        }
    }

    [ServiceContract]
    public interface IEcho
    {
        [OperationContract]
        string Reply(string message);
    }
}

Configuration

‘Yamata’ is the name of the machine I will be running the Service on.

Now we need to grant Kerberos delegation to this machine. For this, we will connect to the Domain Server and ‘Manage users and computers in Active Directory’. Browse to the computer name (yanaga in my case), in the ‘Computers’ tree, rightclick to select ‘properties’, select the ‘Delegation’ tab. For simplicity, we will Thrust this computer for delegation to any service, but you can tighten it to the service of type ‘HOST’, and limit it to specific ports (8000 in our example) if you want.

Next, we need to add the service principal name (SPN) we used in CreateSPNIdentity (HOST/yamata.marbie.net:8000) to the computer. On the domain controller, we can exete in a dosbox the following command:

setspn -a HOST/yamata.marbie.net:8000 yamata

Verify that there are no double entries, with the command: setspn -L yamata

Double entries will break the kerberos authentication.

The next important thing we need to do is make sure this kerberos ticket is propagated to the machine where we want to run the service. Most often, this can be done by -on the machine where we will run the service- lock the screen, and unlock it by typing in the password. This should cause a refresh of the kerberos tickets. Klist.exe or Kerbtray.exe can offer you insight on the list of tickets on your machine. The entry SPN we added should be in this list. If you use Kerbtray, you can check if the ‘delegate’ checkbox is checked dor this entry. It should be. If it is not, the ‘Kerberos Delegation’ right has not been granted to the machine for this Service.

Now, we can launch the Servide under a service account, we will use the LocalSystem account on yamata in this example. To do this, we execute the following command in start| Run:

at HH:mm /i cmd.exe

where we replace HH:mm with the current computer time + 1 minute. Wait a minute and a dos box pops up. This box will not run under your local computer user account, but under LocalSystem. In this dos box, we startup the WCFWithKerberos.exe

2. The Client

Code

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceProcess;
using System.ServiceModel.Description;

namespace Client
{
    class Client
    {

        private ChannelFactory factory;
        private WCFWithKerberos.IEcho echoClient;
        private WSHttpBinding myBinding;
        private EndpointAddress ea;

        public Client()
        {
            // Create the biding
            myBinding = new WSHttpBinding();
            myBinding.Security.Mode = SecurityMode.Message;
            myBinding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;

            // disable credential negotiation and the establishment of a security context
            myBinding.Security.Message.NegotiateServiceCredential = false;
            myBinding.Security.Message.EstablishSecurityContext = false;

            // Create the endpoint address and set the SPN identity.
            // The SPN must match the identity of the service's SPN
		ea = new EndpointAddress(new Uri("http://yamata:8000/Echo"), EndpointIdentity.CreateSpnIdentity("HOST/yamata.marbie.net:8000"));
            /* works with
             *  setspn -a HOST/yamata.marbie.net:8000 yamata
             */

        }

        static void Main(string[] args)
        {
            Client c = new Client();
            c.Open();
            c.echoClient.Reply("test");
            c.Close();
        }

        public void Open()
        {
            // Create the proxy during runtime
            factory = new ChannelFactory(myBinding,ea);
            echoClient = factory.CreateChannel();
        }

        public void Close()
        {
            ((IChannel)echoClient).Close();
        }

    }
}

Configuration

It doesn’t matter under which account or where you run the client, as long as we run it under an account or on a workstation that is properly logged in into the domain (received valid kerberos ticket). Open a dos box and launch the Client.exe

The Service should echo ‘test’ in the console and the client should exit without output or errors. Even when we disabled credential negotiation and security context establishment, we have connected our Client to the Service on a secure way by letting WCF handle all the authentication hassle through the delegation of Kerberos.
Running the Service under a Domain Account

Configuration


There is nothing that needs to be done on the code side, if we re-use the same SPN.

We need to remove the SPN entry from the computer on the active directory, with the SetSPN tool:

setspn -d HOST/yamata.marbie.net:8000 yamata

Now we add the SPN to the domain account.

setspn -a HOST/yamata.marbie.net:8000 marbie\dreezst

where ‘dreezst’ is the domain user and ‘marbie’ is the domain name, under which I will be running the Service.

Additionally, we need to go to the Active Directory again, browse to the user ‘dreezst’, and grant kerberos delegation.

Next, we need to update our kerberos tickets on our Service machine by locking and unlocking the Computer again. Use Klist or Ktray to inspect your kerberos tickets and see that the SPN entry is there and updated. With Ktray, verify the delegation checkbox.

Now, we need to run the service as our domain account. In a dos box, enter: runas /user:marbie\dreezst WCFWithKerberos.exe, and enter the password for the domain user.

Once the Service is up and listening, launch the client from any machine under any (logged in) account.

The difference here is that we are running the Service under a domain account, which will probably be more likely in a production environment where you typically have domain accounts that are configured as Service accounts to run these services.