During the next month we'll be unleashing a slew of webcasts covering architecture and application development for Windows Vista. I've pulled out the sessions of interest for WCF developers. Each session lasts one hour. You can click on the title links to get more details about the presenter and the session.
The Lifetime of a Message in Windows Communication Foundation8 AM Pacific Thursday, June 1, 2006
A Sneak Preview of Windows Communication Foundation from an Early Adopter's Perspective11 AM Pacific Monday, June 5, 2006
Taking Advantage of TCP/IP Reliability in SOAP8 AM Pacific Tuesday, June 6, 2006
Extending Windows Communication Foundation8 AM Pacific Wednesday, June 7, 2006
Introducing Web Services Enhancements for Microsoft .NET (WSE) 3.08 AM Pacific Thursday, June 8, 2006
Dissecting Contract-First Web Services8 AM Pacific Thursday, June 14, 2006
Transactions in Distributed Solutions with Windows Communication Foundation8 AM Pacific Friday, June 15, 2006
Building Powerful AJAX-Style Solutions with ASP.NET "Atlas" and Windows Communication Foundation8 AM Pacific Tuesday, June 20, 2006
Exposing Your Content as a Service Using Windows Communication Foundation8 AM Pacific Wednesday, June 21, 2006
Web Services Interoperability with Java and J2EE Using Windows Communication Foundation ("Indigo")8 AM Pacific Thursday, June 22, 2006
Understanding Windows Communication Foundation Contracts8 AM Pacific Wednesday, June 28, 2006
The versioning of a message in WCF is a combination of the versioning of the envelope format and the addressing format. In Beta 2, the versioning story is a little out-of-date from where it's going to be for the final release. That's simply due to the lag time of producing an official release. I'm going to talk about the Beta 2 versioning story in this post because that's what you can write code against at this time. Everything here is still going to be important in the future.
The addressing format is a transport-neutral mechanism for talking about the location or destination of services and messages. The mechanism for addressing in WCF is Web Services Addressing. There's two version of the WS-Addressing standard to worry about. The first version is the original August 2004 release. The second version, which is what you should really be thinking about, is the final WS-Addressing 1.0 release that came out earlier this month. This version has been tracked by WCF during development.
public sealed class AddressingVersion{ public static AddressingVersion WSAddressing10 { get; } public static AddressingVersion WSAddressingAugust2004 { get; } public override bool Equals(object obj); public override int GetHashCode(); public override string ToString();}
The envelope format describes the contents of a message and how to process it. The standard envelope format for WCF is the Simple Object Access Protocol (SOAP). SOAP is an XML-based packaging format for data. By itself, SOAP doesn't really do much. SOAP relies on other web service standards to plug in and provide functionality that people want, like security, reliability, and transactions. SOAP comes in both a 1.1 flavor from May 2000 and a 1.2 flavor from June 2003. SOAP 1.2 fixes a lot of crufty bits from the earlier specification.
public sealed class EnvelopeVersion{ public string NextDestinationActorValue { get; } public static EnvelopeVersion Soap11 { get; } public static EnvelopeVersion Soap12 { get; } public override int GetHashCode(); public string[] GetUltimateDestinationActorValues(); public override string ToString();}
The message version is just a pairwise combination of the addressing and envelope formats. By default, you get the latest and greatest version of each specification. If you supply an envelope format without an addressing format, you'll get WS-Addressing 1.0. If you don't supply anything, you'll get WS-Addressing 1.0 with SOAP 1.2.
public sealed class MessageVersion{ public AddressingVersion Addressing { get; } public static MessageVersion Default { get; } public EnvelopeVersion Envelope { get; } public static MessageVersion Soap11WSAddressing10 { get; } public static MessageVersion Soap11WSAddressingAugust2004 { get; } public static MessageVersion Soap12WSAddressing10 { get; } public static MessageVersion Soap12WSAddressingAugust2004 { get; } public static MessageVersion CreateVersion(EnvelopeVersion envelopeVersion); public static MessageVersion CreateVersion(EnvelopeVersion envelopeVersion, AddressingVersion addressingVersion); public override bool Equals(object obj); public override int GetHashCode(); public override string ToString();}
Next time: Inside the Standard Bindings: BasicHttp
There are two ways to think about the representation of data on a standard Ethernet network. The first way is to think about the actual encoded electronic pulses that go across the wire. This turns out to be very complicated because the signaling not only has to capture the logical information of the data, but also accommodate the timing, noise, and efficiency characteristics of the adapters and wires. There have been a number of encodings developed for this purpose, but unless you're building physical hardware, you don't ever need to see this encoded form.
Let's not think about what the encoded pulses look like. Instead, let's just think about the logical data that is being sent to the adapter. Even if you never write a device driver for your network card, you still might come across logical Ethernet frames if you ever need to directly read or write network data.
An Ethernet frame has a 14 byte header, a data section, and a 4 byte trailer. The header contains the address of the machine that should read the frame, the address of the machine that sent the frame, and the type of data payload. The destination address is mostly a suggestion. There's nothing stopping other machines on the network from reading the data as well. Note that the source address is the source on this network. That machine may have originally gotten the data from somewhere else. The actual source address is hopefully buried in the data.
There are hundreds of recognized formats for the data section. Your network card probably only ever sees two of those: IPv4 and ARP. If you're adventurous, maybe it gets some RARP and IPv6 data as well. Many of the registered formats haven't been used in years. The Internet Protocol won the protocol wars.
The contents of the data are an opaque blob to the Ethernet adapter. Inside that data is typically more layers of framing and encapsulation before you get to the application data. However, the network driver may need to attach some null bytes to the end of the data to meet the minimum size requirements of an Ethernet frame. This requirement comes from those electrical characteristics that we weren't going to talk about. The length needs to be included inside the data so that the pad bytes can be ignored. Oddly, even though every driver must pad data on the way out, a network driver reading data can't assume that the padding exists. This is because going through a loopback device may have stripped out the padding and then indicated the frame to another adapter on the machine. The frame never went over the wire in that state but the driver doesn't know that.
The CRC is a hashing function that checks that data wasn't corrupted during transmission. This isn't used as a security measure because anyone can recomputed the hash easily. The CRC just protects against unintentional modifications. I'll talk more about the CRC and how to compute it in a few weeks.
Next time: Versioning for Addresses, Envelopes, and Messages
A CustomBinding defines a binding by providing a thin wrapper around a collection of binding elements. Custom bindings don't have any of the niceities of a handcrafted binding, such as properties that provide direct control over the binding elements and their settings. However, in return, a custom binding is very cheap to create and doesn't require defining a new class.
The bulk of the methods for the CustomBinding class deal with importing the binding elements for the binding.
public class CustomBinding : Binding, ISecurityCapabilities{ public CustomBinding(); public CustomBinding(Binding binding); public CustomBinding(IEnumerable<BindingElement> bindingElementsInTopDownChannelStackOrder); public CustomBinding(params BindingElement[] bindingElementsInTopDownChannelStackOrder); public CustomBinding(string configurationName); public CustomBinding(string name, string ns, params BindingElement[] bindingElementsInTopDownChannelStackOrder); public BindingElementCollection Elements { get; } public override string Scheme { get; } public override BindingElementCollection CreateBindingElements();}
Each constructor provides a different way to specify the contents of the binding. After defining the custom binding, you can still access the binding elements for modification through the Elements collection. The CreateBindingElements method is the standard way for consuming the binding elements of the binding. In this case, calling CreateBindingElements() simply returns a collection containing your specified binding elements. However, you can't change the binding through this collection because it's only a copy of the elements that are inside.
You specify the order of binding elements in the same order that you want for your channel stack. There's a rough order that typically occurs:
Intermixed in these binding elements can be shape changers that change the apparent structure of the underlying channels. I won't get into the topic today, but you generally want to push shape changers as far down your channel stack as you can.
Here's an example of the binding element stack using the WSDualHttpBinding:
System.ServiceModel.Channels.TransactionFlowBindingElementSystem.ServiceModel.Channels.ReliableSessionBindingElementSystem.ServiceModel.Channels.SymmetricSecurityBindingElementSystem.ServiceModel.Channels.CompositeDuplexBindingElementSystem.ServiceModel.Channels.TextMessageEncodingBindingElementSystem.ServiceModel.Channels.HttpTransportBindingElement
That's two conversation modifiers, a message modifier, a shape changer, the message encoder, and finally the transport.
Next time: What Data Looks Like on an Ethernet Network
This article is now a part of the Windows SDK.
This is the last planned article in a documentation series covering various aspects of Windows Communication Foundation transports. Today's topic covers how to choose a transport and associated encoder.
The Windows Communication Foundation (WCF) programming model separates the behavior of endpoint operations from the transport mechanism that connects two endpoints. This gives you flexibility when deciding how your services should be exposed to the network. Transports and message encoders are pieces of WCF that sit below a service to provide connectivity. This document discusses some of the characteristics you need to evaluate when choosing a transport and message encoding.
In scenarios where you must connect to a preexisting client or server, you may not have a choice about using a particular transport and message encoding. However, WCF services can be made accessible through multiple endpoints, each with a different transport or message encoding. When a single selection does not cover the entire intended audience for your service, you should consider exposing your service over multiple endpoints. Client applications can then choose the endpoint that is most favorable for them. Using multiple endpoints allows you to combine the advantages of different transports and message encoders.
In WCF, the transfer of data across a network requires the joint cooperation of a transport and message encoder.
A message encoder converts a System.ServiceModel.Channels.Message to a serialized form. This document covers the Text, Binary, and MTOM message encoders that are included in WCF. The text message encoder supports both a plain XML encoding as well as SOAP encodings. The plain XML encoding mode of the text message encoder is called the POX encoder to distinguish it from the text-based SOAP encoding.
A transport sends the serialized form of the message to another application. This document covers the HTTP, TCP, and named pipe transports that are included in WCF. WCF provides several standard bindings that combine a transport, message encoder, and other options. For instance, the BasicHttpBinding binding combines the HTTP transport with a text message encoder. Similarly, the NetTcpBinding binding combines the TCP transports with a binary message encoder. You are not limited to choosing a preset combination given by a standard binding.
The following table describes several decision points commonly used when choosing a transport. You should add new attributes and transports to this list as you identify them. The general process you should use is to identify the attributes that are important for your application, identify the transports that associate favorably with each of your chosen attributes, and then select the transports that work best with your attribute set.
Attribute
Description
Favored Transports
Connectivity
The connectivity of a transport reflects how capable the transport is at reaching other systems. The named pipe transport has very little reach; it can only connect to services running on the same machine. The TCP and HTTP transports both have excellent reach and can penetrate some NAT and firewall configurations. See the Working with NATs and Firewalls document for more details.
HTTP, TCP
Diagnostics
Diagnostics allow you to automatically detect connectivity problems with a transport. The named pipe transport generally has easier to diagnose errors because both endpoints of the connection must be on the same machine. All transports support the ability to send back fault information that describes connectivity. However, WCF does not come with diagnostic tools for investigating network issues.
Hosting
All WCF endpoints need to be hosted inside of some application. IIS 6.0 and earlier only support hosting applications that use the HTTP transport. On Windows Vista, support is added for hosting all WCF transports, including the TCP and named pipe transports. See the documentation for Hosting in IIS and Hosting in WAS for more details.
HTTP
Inspection
Inspection is the ability to examine messages during transmission. The HTTP protocol separates routing and control information from data, making it easier to build tools that inspect and analyze messages. The level of security used will impact whether messages can be inspected.
Latency
Latency is the minimum amount of time required to complete an exchange of messages. All network operations have some latency, although this amount can be increased by the choice of transport. Using duplex or one-way communication with a transport whose native message exchange pattern is request-reply, such as HTTP, can cause additional latency due to the forced correlation of messages. In this situation, consider using a transport whose native message exchange pattern is duplex, such as TCP.
TCP, named pipe
Security
Security is the capability of protecting messages during transfer, either by supplying confidentiality, integrity, or authentication. Confidentiality protects a message from being examined, integrity protects a message from being modified, and authentication gives assurances about the sender or receiver of the message. WCF supports transfer security both at the message level and transport level. Message security will compose with any transport as long as the transport supports a buffered transfer mode. The support for transport security will vary depending on the chosen transport. The HTTP, TCP, and named pipe transports have reasonable parity in their support for transport security.
All
Throughput
Throughput measures the amount of data that can be transmitted and processed in a specified period of time. Like latency, the throughput for service operations can be affected by the chosen transport. Maximizing throughput for a transport requires minimizing both the overhead of transmitting content as well as minimizing the time spent waiting for message exchanges to complete. Both the TCP and named pipe transports add little overhead to the message body and support a native duplex shape that reduces the need to wait for message replies.
Tooling
Tooling represents the support for a protocol by third-party applications for development, diagnosis, hosting, and other activities. There has been a particularly large investment in providing tools and software for working with the HTTP protocol.
The following table describes several decision points commonly used when choosing a message encoder. You should add new attributes and message encoders to this list as you identify them. The general process you should use is to identify the attributes that are important for your application, identify the message encoders that associate favorably with each of your chosen attributes, and then select the message encoders that work best with your attribute set.
Favored Encodings
Inspection is the ability to examine messages during transmission. Text encodings, either with or without the use of SOAP, allow messages to be inspected and analyzed by many applications without the use of specialized tools. Note that the use of transfer security, at either the message or transport level, will impact your ability to inspect messages. Confidentiality protects a message from being examined and integrity protects a message from being modified.
Text, POX
Reliability
Reliability is the resiliency that an encoding has to errors in transmission. Reliability can also be provided at the message, transport, or application level. All of the standard WCF encodings assume that reliability is being provided at some other layer. There is generally little ability for the encoding to recover from an error in transmission.
Simplicity
Simplicity represents with which encoders and decoders can be created for an encoding specification. Text encodings are particularly advantageous for simplicity, and the POX encoder has the additional advantage of not requiring support for processing SOAP.
POX
Size
The encoding size is the amount of overhead imposed on content by the message encoding. The size of encoded messages is directly related to the maximum throughput of service operations. Binary encodings are generally more compact than text encodings. When message size is at a particular premium, you may consider additionally compressing the message contents during encoding. However, compression adds additional processing costs for both the message sender and receiver.
Binary
Streaming
Streaming allow applications to begin processing a message before the entire message has arrived. Effectively using streaming requires that the important data for a message be available at the beginning of the message so that the receiving application does not need to wait for it to arrive. Moreover, applications that use streamed transfer must organize data in the message incrementally so that the content does not have forward dependencies. In many cases, there is a tradeoff between being able to stream content and having the smallest possible transfer size for that content.
Tooling represents the support for an encoding by third-party applications for development, diagnosis, and other activities. There has been a particularly large investment in providing tools and software for working with messages encoded in the POX format.
Next time: Creating Custom Bindings
Unless you've somehow been avoiding all of the announcements that have come out today, then you know that today Windows Vista, WinFX, and Office 2007 all had their Beta 2 release today. The Beta 2 version for WCF was snapped not too long after the February CTP release went out. This means that there's only a small set of incompatibilities to deal with in this release.
To upgrade, you'll want to uninstall the February CTP components, and then get:
Today's post is a light entrée covering the IDefaultCommunicationTimeouts interface. This interface bundles up the standard group of timeouts- open, close, receive, and send- into one convenient package. A surprising number of classes implement or consume IDefaultCommunicationTimeouts, usually resulting in a series of several delegations in order to reach where the timeouts are actually set.
public interface IDefaultCommunicationTimeouts{ TimeSpan CloseTimeout { get; } TimeSpan OpenTimeout { get; } TimeSpan ReceiveTimeout { get; } TimeSpan SendTimeout { get; }}
Methods that take an IDefaultCommunicationTimeouts parameter tend to have a quirky heritage. This usually indicates that they at one point were expecting a more substantial class or interface, but that over time, we whittled away the other requirements until only this interface remained.
I don't get a lot of direct use out of this interface because I primarily program against the channel model. When you use channels directly, you have the ability to explicitly specify timeouts on a per-channel, per-operation, per-whatever basis. Timeouts are very much in your face when using the channel model meaning that they're also very accessible when you want to control them. When you're programming against services and contracts though, timeouts are more in the background and the infrastructure machinery has to flow timeouts from place to place on your behalf. IDefaultCommunicationTimeouts is one mechanism for providing that flow.
Next time: Choosing a Transport
A BufferManager recycles the byte buffers used when reading and writing buffered messages. There's some allocation overhead creating these frequently used buffers, making buffer recycling a net win in high-throughput scenarios. As you move to larger message sizes though, buffer recycling becomes less of a factor and then eventually a net loss. All of this is encoded in the BufferManager class so that you don't have to think about it.
When you create a BufferManager, you specify the size of the heap to draw from. Once the total concurrent allocations exceed this heap size, the buffer manager begins returning non-recyclable buffers. The second tunable parameter of a BufferManager is the maximum size of a recyclable buffer. Again, if an allocation exceeds this maximum size, then the buffer manager simply returns a non-recyclable buffer. This limit prevents the buffer manager from holding blocks of memory for large messages, which tends to be inefficient.
public abstract class BufferManager{ protected BufferManager(); public abstract void Clear(); public static BufferManager CreateBufferManager(long maxBufferPoolSize, int maxBufferSize); public abstract void ReturnBuffer(byte[] buffer); public abstract byte[] TakeBuffer(int bufferSize);}
The basic functionality of a buffer manager is very similar to a heap with explicit malloc and free. You grab a buffer using TakeBuffer and later give it back using ReturnBuffer. You cannot return someone else's memory using ReturnBuffer. The most common source of this is if you allocate your own byte array or pull from another buffer manager, and then give that memory to a message encoder. This will fail after the encoder is done with the buffer. Here's what that failure exception tends to look like:
Unhandled Exception: System.ArgumentException: This buffer can not be returned to the buffer manager because it is the wrong size.Parameter name: buffer at System.ServiceModel.Channels.BufferManager.PooledBufferManager.ReturnBuffer(Byte[] buffer) at System.ServiceModel.Channels.BufferedMessageData.DoClose() at System.ServiceModel.Channels.BufferedMessageData.Close() at System.ServiceModel.Channels.BufferedMessage.OnClose() at System.ServiceModel.Channels.Message.Close() at System.ServiceModel.Channels.Message.System.IDisposable.Dispose()
The final method in the BufferManager class is Clear, which allows you to flush the cache. You might call Clear when your server goes idle after handling enough messages to have touched a lot of buffers, although I haven't seen a frequent need for applications to do this.
Next time: Introducing IDefaultCommunicationTimeouts
Last time I was looking at hashing algorithms when I pointed out that finding collisions was easier than reversing a message digest. For a good hashing algorithm, finding a message with a particular digest generally requires looking at as many messages as the size of the message digest space. If you have a 64-bit message digest, then you need to look at 2^64 messages, more or less (a little less actually).
It turns out that it takes looking at far fewer messages on average to find a collision. The cause of this is something called the birthday paradox. The birthday paradox comes from the question whether any two people in a crowded room were born on the same day of the year. Most people guess that everyone has a unique birthday unless there's at least a hundred people in the room. In reality, it turns out that it only takes about two dozen people before it's more likely than not that two share a common birthday. Here's why.
Take M to be the size of the message digest space, say 2^64 or so. Then, the probability that there's a collision in a group of N messages is one minus the chance of every message having a unique message digest.
Now, this is an inconvenient thing to try to solve for particular values of M and N. What we can do is take an approximation to that product to make it easier to evaluate.
I've used an expansion of e^x to first convert the terms of the product and then collapse all of the terms together into a simple expression. We still need to solve for N though to find out how difficult it is to come up with collisions.
I put 0.5 in as the probability I was looking for, and then found an expression for N in terms of a constant times the square root of M. This shows that you only have to look at slightly more than 2^32 messages to find a collision for a 64-bit message digest. On the other hand, you need to look at slightly less than 2^64 messages to reverse a particular message digest. This is why collisions are so much harder to protect against than reversibility.
Next time: Using the BufferManager
In the Basics of Transport Security article I wrote a few weeks ago, I introduced three different kinds of security that people care about for their messages. Let's look at the concepts behind implementing two of those types of security.
Confidentiality means keeping the contents of the message secret from unintended listeners. This is usually what people think of when you talk about security. The easily recognized way of providing confidentiality is message encryption. An encryption algorithm takes the message, called the plaintext, and transforms that into ciphertext. The information that randomizes the encryption process is called the key. Ciphertext is essentially indistinguishable from random data, and the ciphertext of a particular message is essentially indistinguishable from the ciphertext of a random message. Decryption is the process of returning the plaintext from the ciphertext.
When the encryption and decryption processes use the same key, that key must be a shared secret between the sender and receiver of the message. This is called secret key or symmetric cryptography. An alternative is to use separate keys for encryption and decryption, allowing you to publish your encryption key for other people to use. This means that anyone can send you a message without sharing a secret with you, but nobody else can read the messages intended for you. This is called public key or asymmetric cryptography. Breaking message security is easier for a given key length when using asymmetric cryptography. Public keys need to be larger in size than a shared secret to provide an equivalent level of protection.
A good encryption algorithm is resistant to finding the plaintext if you don't know the decryption key. A good encryption algorithm is also resistant to finding the key or remaining plaintext even if you know some of the plaintext for a particular message.
Integrity means having confidence that the message you received is the same as the message that the sender sent. An encryption algorithm does not provide any guarantees of integrity. If you were to modify the ciphertext of a message, then the receiver could still decrypt that message into some plaintext. You don't know how that plaintext would relate to the original plaintext message, if it does at all. For many encryption algorithms, the resulting plaintext from making changes to an encrypted message results in gibberish. However, if the receiver does not know what the message was supposed to look like, they may not realize that is has been tampered with.
A hashing algorithm takes an arbitrary length message and produces a fixed-length output that is characteristic of the message. This is also called a message digest. A hashing algorithm should be irreversible, meaning that producing any portion of the message should be impossible given only the message digest.
There are two measures of the effectiveness of a hashing algorithm. The first is how difficult it is to take a message digest and produce a message that hashes to that digest value. It's still bad if you can find a different message that hashes to the digest value than was originally used to create the message digest. Many password schemes apply hashing to the input password so that they don't have to pass around the original. Since the hashing algorithm is hard to reverse, knowing the password digest does not allow an attacker to gain access to the protected resource. However, any password that hashes to the same digest value would let the attacker access the resource.
The second measure of effectiveness for a hashing algorithm is how difficult it is to find any two messages that have the same digest value. This is called the collision-resistance of the hashing algorithm. It turns out that finding collisions is often much easier than reversing a message digest. This means that the digest strength of the hashing algorithm is frequently chosen based on how bad it would be if someone could produce collisions. I'll talk next time about how easy it is in general to find collisions for a hashing algorithm.
Next time: Math Behind the Hashing Birthday Attack
Here's a quick rundown of what you need to get started writing a custom channel for WCF. It doesn't matter whether you're writing a layered channel or transport, everything here is good to know about.
Tools You Need
Everyone needs the WinFX runtime for running applications. If you have beta of Windows Vista, this may already be installed on your machine. I'd recommend using the Microsoft Pre-Release Software WinFX Runtime Components - February Community Technology Preview (CTP) version until a newer CTP comes along.
You will need the Windows Platform SDK to get documentation and samples. The matching SDK is the Microsoft® Windows® Software Development Kit (SDK) for the February 2006 Community Technology Preview (CTP) for Windows Vista and WinFX Runtime Components. Yes, that's really the name of it.
I would strongly recommend having Visual Studio 2005. The reference documentation for Windows Vista in the February CTP is incomplete, making it very important to have Intellisense support. You can get the Microsoft Visual Studio Code Name “Orcas” Community Technology Preview - Development Tools for WinFX® tools addon, although I personally don't use this for development.
Blogs to Read
I'm assuming that you've found my blog. You should look through the Samples and Channels categories for information about writing a custom channel. Especially useful is the custom transport walkthrough overview, which is still helpful even if you're writing a layered channel instead of a transport.
You should look at Yasser Shohoud's blog and Kenny Wolf's blog as well. Kenny does hints and tips while Yasser is more of a big picture writer.
Forums to Post On
There's really only one that I've seen get a lot of participation. Ask your questions on the Windows Communication Foundation ("Indigo") forum on forums.microsoft.com.
Samples to Look At
There's three more samples you want to look at when writing a custom channel.
Get the WSE 3.0 TCP Interop sample from windowscommunication.net. This is a custom transport that is basically TCP with a few framing tweaks.
Get the UDP sample from the Windows SDK. After installation, look in the SDK at Microsoft SDKs\Windows\v1.0\samples for a zip file called AllWinFXsamples.zip. The UDP sample is inside the zip file at WindowsCommunicationFoundation\TechnologySamples\Extensibility\Transport\Udp\CS. This is a very complete custom transport implementation.
Get the Chunking and Streaming Custom Channel sample from windowscommunication.net. This is a layered channel that supports fragmenting and reassembling messages.
Next time: Basics of Encryption and Hashing
One of the advantages of using WCF is that you can change the network protocol without changing how your application works. Let's show that off a bit while also looking at what the counting message encoder stats look like for some common scenarios.
Last time, we looked at the file transport using a streamed transfer. Here's the same transport with the transfer mode changed to use buffered messages.
CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new TextMessageEncodingBindingElement()); FileTransportBindingElement transport = new FileTransportBindingElement(); transport.Streamed = false; Uri uri = new Uri("my.file://localhost/x");
Creating factory... done. Creating channel... done. Enter some text: abcd Sending request... done. Processing reply: http://reflection Reply: dcba Read 355 bytes in 1 operations. Wrote 352 bytes in 1 operations. Creating listener... done. Creating channel... done. Waiting for request... done. Processing request: http://reflect Sending reply... done. Read 352 bytes in 1 operations. Wrote 355 bytes in 1 operations. Waiting for request...
You'll notice that the number of bytes read and written didn't change, but everything completes in a single operation. That's because buffered messaging hands off the entire message at one time. When might you see buffered messaging take more than one operation to complete a transfer? If you're using chunking or reliable messaging, a single top-level call can result in multiple underlying operations.
Now, let's flip over to using TCP/IP and change the encoding from text to binary-encoded XML.
CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new BinaryMessageEncodingBindingElement()); TcpTransportBindingElement transport = new TcpTransportBindingElement(); transport.TransferMode = TransferMode.Streamed; Uri uri = new Uri("net.tcp://localhost:5555/");
Creating factory... done. Creating channel... done. Enter some text: abcd Sending request... done. Processing reply: http://reflection Reply: dcba Read 108 bytes in 35 operations. Wrote 137 bytes in 1 operations. Creating listener... done. Creating channel... done. Waiting for request... done. Processing request: http://reflect Sending reply... done. Read 137 bytes in 43 operations. Wrote 108 bytes in 1 operations. Waiting for request...
The byte count went way down while the operation count went way up. That's because the binary encoder does a lot of single byte reads before deciding how to interpret the upcoming data.
Let's jump from TCP/IP to named pipes while keeping the same encoding.
CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new BinaryMessageEncodingBindingElement()); NamedPipeTransportBindingElement transport = new NamedPipeTransportBindingElement(); transport.TransferMode = TransferMode.Streamed; Uri uri = new Uri("net.pipe://localhost/x");
Creating factory... done. Creating channel... done. Enter some text: abcd Sending request... done. Processing reply: http://reflection Reply: dcba Read 108 bytes in 35 operations. Wrote 134 bytes in 1 operations. Creating listener... done. Creating channel... done. Waiting for request... done. Processing request: http://reflect Sending reply... done. Read 134 bytes in 43 operations. Wrote 108 bytes in 1 operations. Waiting for request...
Virtually the same results, although the total byte count differs slightly. I'll let you figure out why that is. You can find out by switching to the text encoding and comparing the SOAP for the two transports.
Speaking of text, now we'll switch back to that encoding but over HTTP. I've got IIS running on port 80 already so I need to specify a different port for running my service.
CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new TextMessageEncodingBindingElement()); HttpTransportBindingElement transport = new HttpTransportBindingElement(); transport.TransferMode = TransferMode.Streamed; Uri uri = new Uri("http://localhost:5555/x");
Creating factory... done. Creating channel... done. Enter some text: abcd Sending request... done. Processing reply: http://reflection Reply: dcba Read 300 bytes in 7 operations. Wrote 354 bytes in 1 operations. Creating listener... done. Creating channel... done. Waiting for request... done. Processing request: http://reflect Sending reply... done. Read 354 bytes in 8 operations. Wrote 300 bytes in 1 operations. Waiting for request...
We haven't used the MTOM encoder yet, so let's keep on with HTTP but switch the encoder again.
CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new MtomMessageEncodingBindingElement()); HttpTransportBindingElement transport = new HttpTransportBindingElement(); transport.TransferMode = TransferMode.Streamed; Uri uri = new Uri("http://localhost:5555/x");
Creating factory... done. Creating channel... done. Enter some text: abcd Sending request... done. Processing reply: http://reflection Reply: dcba Read 779 bytes in 2 operations. Wrote 833 bytes in 3 operations. Creating listener... done. Creating channel... done. Waiting for request... done. Processing request: http://reflect Sending reply... done. Read 833 bytes in 1 operations. Wrote 779 bytes in 3 operations. Waiting for request...
Wow, the byte count really spiked when we changed the encoding. The operation count dipped to the lowest point we've seen for streamed transfers though. Which of the two is more important? BYTES. Even though network operations are typically expensive, we can create a readahead buffer for the stream so that all of the small reads are satisfied without hitting the network.
Next time: Resources for Channel Authors
After a short break we're back to working on the custom message encoder. The complete source code for the encoder is available in Part 1 and Part 2 of this series. Today and tomorrow I'll be performing some runs using the encoder to show how it works. We'll need a sample client and server application to host the encoder. I've dusted off the code I put together for the FileTransport to fill this role.
using System; using System.ServiceModel.Channels; using CountingEncoder; using FileTransport; namespace Server { class Server { static void Main(string[] args) { Console.Write("Creating listener..."); CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new TextMessageEncodingBindingElement()); FileTransportBindingElement transport = new FileTransportBindingElement(); transport.Streamed = true; CustomBinding binding = new CustomBinding(encoder, transport); Uri uri = new Uri("my.file://localhost/x"); IChannelListener<IReplyChannel> listener = binding.BuildChannelListener<IReplyChannel>(uri, new BindingParameterCollection()); listener.Open(TimeSpan.FromSeconds(5)); Console.WriteLine(" done."); Console.Write("Creating channel..."); IReplyChannel channel = listener.AcceptChannel(TimeSpan.FromSeconds(5)); channel.Open(TimeSpan.FromSeconds(5)); Console.WriteLine(" done."); Console.Write("Waiting for request..."); while (channel.WaitForRequest(TimeSpan.FromMinutes(1))) { using (IRequestContext context = channel.ReceiveRequest(TimeSpan.FromSeconds(5))) { Console.WriteLine(" done."); using (Message message = context.RequestMessage) { Console.WriteLine("Processing request: {0}", message.Headers.Action); if (message.Headers.Action == "http://reflect") { string response = ProcessReflectRequest(message.GetBody<string>()); Console.Write("Sending reply..."); Message replyMessage = Message.CreateMessage(MessageVersion.Default, "http://reflection", response); context.Reply(replyMessage, TimeSpan.FromSeconds(5)); Console.WriteLine(" done."); } } } Console.Out.WriteLine("Read {0} bytes in {1} operations.", encoder.ReadBytes, encoder.ReadCount); Console.Out.WriteLine("Wrote {0} bytes in {1} operations.", encoder.WriteBytes, encoder.WriteCount); Console.Write("Waiting for request..."); } Console.WriteLine(" terminated."); channel.Close(TimeSpan.FromSeconds(5)); } static string ProcessReflectRequest(string request) { char[] output = new char[request.Length]; for (int index = 0; index < request.Length; index++) { output[index] = request[request.Length - index - 1]; } return new string(output); } } }
using System; using System.ServiceModel; using System.ServiceModel.Channels; using CountingEncoder; using FileTransport; namespace Client { class Client { static void Main(string[] args) { Console.Write("Creating factory..."); CountingEncoderBindingElement encoder = new CountingEncoderBindingElement(new TextMessageEncodingBindingElement()); FileTransportBindingElement transport = new FileTransportBindingElement(); transport.Streamed = true; CustomBinding binding = new CustomBinding(encoder, transport); IChannelFactory<IRequestChannel> factory = binding.BuildChannelFactory<IRequestChannel>(); factory.Open(TimeSpan.FromSeconds(5)); Console.WriteLine(" done."); Console.Write("Creating channel..."); using (factory) { Uri uri = new Uri("my.file://localhost/x"); IRequestChannel channel = factory.CreateChannel(new EndpointAddress(uri)); Console.WriteLine(" done."); Console.Write("Enter some text: "); String text = Console.ReadLine(); if (text == null) { return; } Console.Write("Sending request..."); Message requestMessage = Message.CreateMessage(MessageVersion.Default, "http://reflect", text); channel.Open(TimeSpan.FromSeconds(5)); Message replyMessage = channel.Request(requestMessage, TimeSpan.FromSeconds(5)); Console.WriteLine(" done."); using (replyMessage) { Console.WriteLine("Processing reply: {0}", replyMessage.Headers.Action); Console.WriteLine("Reply: {0}", replyMessage.GetBody<string>()); } channel.Close(TimeSpan.FromSeconds(5)); } Console.Out.WriteLine("Read {0} bytes in {1} operations.", encoder.ReadBytes, encoder.ReadCount); Console.Out.WriteLine("Wrote {0} bytes in {1} operations.", encoder.WriteBytes, encoder.WriteCount); } } }
In the code here, I've created a binding with the file transport and text message encoder. I've set the transfer mode to use streaming for this example. The counting encoder wraps around the text encoder to inspect the bytes as they go to the transport. After completing an operation, the client and server both print the number of bytes they sent and received as well as the number of individual calls required.
Here's what I get when running this on the server:
Creating listener... done. Creating channel... done. Waiting for request... done. Processing request: http://reflect Sending reply... done. Read 352 bytes in 8 operations. Wrote 355 bytes in 1 operations. Waiting for request...
And, here's what I get when running this on the client:
Creating factory... done. Creating channel... done. Enter some text: abcd Sending request... done. Processing reply: http://reflection Reply: dcba Read 355 bytes in 8 operations. Wrote 352 bytes in 1 operations.
Notice that a single send operation has eight read operations on the other side of the connection. The way that the data is framed and written by the send side of the connection does not put any requirements on how the receive side must consume the data. Some transports have this kind of requirement at the network level, but you generally cannot see any impact in your application.
Next time: Building A Custom Message Encoder to Record Throughput, Part 4
Authentication is the process of identifying whether a client is eligible to access a resource. The HTTP protocol supports authentication as a means of negotiating access to a secure resource.
The initial request from a client is typically an anonymous request, not containing any authentication information. HTTP server applications can deny the anonymous request while indicating that authentication is required. The server application sends WWW-Authentication headers to indicate the supported types of authentication scheme. This document describes several authentication schemes for HTTP and discusses their support in Windows Communication Foundation (WCF).
HTTP Authentication Schemes
The server can specify multiple authentication schemes for the client to choose from. The following list describes some of the authentication schemes commonly found in Windows applications.
Authentication Scheme
Anonymous
An anonymous request does not contain any authentication information. This is equivalent to granting everyone access to the resource.
Basic
Basic authentication sends a Base64 encoded string that contains a user name and password for the client. Base64 is not a form of encryption and should be considered the same as sending the user name and password in clear text. If a resource needs to be protected, strongly consider using an authentication scheme other than basic authentication.
Digest
Digest authentication is a challenge-response scheme that is intended to replace Basic authentication. The server sends a data string called a nonce to the client as a challenge. The client responds with a hash that includes the user name, password, and nonce, among additional information. Using hashing and the nonce makes it more difficult to steal and reuse the user's credentials with this authentication scheme.
Digest authentication requires the use of Windows domain accounts. The digest realm indicates the Windows domain name. Due to this, a server running on an operating system that does not support Windows domains, such as Windows XP Home, cannot be used with Digest authentication. When the client is running on an operating system that does not support Windows domains, a domain account must be explicitly specified during the authentication.
Ntlm
Ntlm authentication is a challenge-response scheme that is a more secure variation of Digest authentication. Ntlm uses Windows credentials to transform the challenge data instead of the unencoded user name and password. Ntlm authentication requires multiple exchanges between the client and server. The server and any intervening proxies must support persistent connections to successfully complete the authentication.
Negotiate
Negotiate authentication automatically selects between Kerberos and Ntlm authentication depending on availability. Kerberos is used in preference if available, otherwise Ntlm is tried instead. Kerberos authentication significantly improves upon Ntlm. Using Kerberos authentication is both faster than Ntlm and allows the use of mutual authentication and delegation of credentials to remote machines.
Passport
The underlying Windows HTTP service includes authentication using federated protocols. However, the standard HTTP transports in WCF do not support the use of federated authentication schemes, such as Microsoft Passport. Support for this feature is currently available through the use of message security.
Choosing an Authentication Scheme
When selecting the potential authentication schemes for an HTTP server, first consider whether the resource needs to be protected. Using HTTP authentication requires transmitting more data and can limit interoperability with clients. Allow anonymous access to resources that do not need to be protected.
If the resource needs to be protected, next consider which authentication schemes provide the required level of security. The weakest standard authentication scheme discussed here is Basic authentication. Basic authentication does not protect the user's credentials. The strongest standard authentication scheme discussed here is Negotiate authentication, resulting in Kerberos. A server should not present an authentication scheme that it is not prepared to accept or that does not adequately secure the protected resource. Clients are free to choose between any of the authentication schemes presented by the server. Some clients default to a weak authentication scheme or the first authentication scheme in the server's list.
The clock continues to count down for the start of TechEd 2006 in Boston. We're now exactly one month away from the kickoff on June 11th. I wouldn't actually recommend showing up at the convention center exactly one month from now. Even though the registration will be open, the keynote isn't until 7 PM so you'd be sitting around for 11 hours with nothing to do but shop at the TechEd store.
Here's something you could do with that time though: I'm looking for topic suggestions for some microtalks to give during the week about channels and transports. Use the email link to send them in or just post your ideas in comments
I'm also looking for suggestions about what you want to see running on this blog while I'm away. I probably won't have regular time to write articles during that week so I may need to put in some filler. Here are your voting options:
I hope you picked option 3 because that's what you're getting. Although if there's a problem getting an Internet connection in Boston, let me tell you now that the handling for null messages is absolutely fascinating.
Next time: Understanding HTTP Authentication
Last time we looked at building a custom message encoder that counted the number of bytes that the transport was reading and writing from the message. We're building it inside out so I started off by making the encoder itself. An encoder comes from a message encoder factory, so now we need to put something together that spits out an instance of the encoder. The encoder doesn't need to keep any state about the message, which means that we can make the encoder a singleton of the factory.
using System.ServiceModel.Channels; namespace CountingEncoder { class CountingEncoderFactory : MessageEncoderFactory { readonly CountingEncoder encoder; readonly MessageEncoderFactory innerFactory; public CountingEncoderFactory(CountingEncoderBindingElement bindingElement, MessageEncoderFactory innerFactory) { this.innerFactory = innerFactory; this.encoder = new CountingEncoder(bindingElement, innerFactory.Encoder); } public override MessageEncoder Encoder { get { return this.encoder; } } public override MessageVersion MessageVersion { get { return this.innerFactory.MessageVersion; } } } }
Now, the factory itself comes from a message encoder binding element. The binding element also is what keeps track of the running counts for data. I put the tracking in the binding element because it's typical to clone the channel stack pieces. By keeping the data in the binding element, you get the correct counts regardless of the instance that was actually used by the channel.
using System.ServiceModel.Channels; namespace CountingEncoder { public class CountingEncoderBindingElement : MessageEncodingBindingElement { readonly CountingEncoderBindingElement baseBindingElement; readonly MessageEncodingBindingElement innerBindingElement; long readBytes; int readCount; long writeBytes; int writeCount; public CountingEncoderBindingElement(MessageEncodingBindingElement innerBindingElement) : base() { this.innerBindingElement = innerBindingElement; } CountingEncoderBindingElement(CountingEncoderBindingElement originalBindingElement) : this(originalBindingElement.innerBindingElement) { if (originalBindingElement.baseBindingElement == null) { this.baseBindingElement = originalBindingElement; } else { this.baseBindingElement = originalBindingElement.baseBindingElement; } } public override AddressingVersion AddressingVersion { get { return this.innerBindingElement.AddressingVersion; } set { this.innerBindingElement.AddressingVersion = value; } } public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context) { context.UnhandledBindingElements.Add(this); return base.BuildChannelFactory<TChannel>(context); } public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context) { context.UnhandledBindingElements.Add(this); return base.BuildChannelListener<TChannel>(context); } public override MessageEncoderFactory CreateMessageEncoderFactory() { return new CountingEncoderFactory(this, innerBindingElement.CreateMessageEncoderFactory()); } public override BindingElement Clone() { return new CountingEncoderBindingElement(this); } public override T GetProperty<T>(BindingContext context) { return innerBindingElement.GetProperty<T>(context) ?? context.GetInnerProperty<T>(); } public long ReadBytes { get { return BaseBindingElement.readBytes; } internal set { BaseBindingElement.readBytes = value; } } public int ReadCount { get { return BaseBindingElement.readCount; } internal set { BaseBindingElement.readCount = value; } } public long WriteBytes { get { return BaseBindingElement.writeBytes; } internal set { BaseBindingElement.writeBytes = value; } } public int WriteCount { get { return BaseBindingElement.writeCount; } internal set { BaseBindingElement.writeCount = value; } } CountingEncoderBindingElement BaseBindingElement { get { return this.baseBindingElement ?? this; } } } }
The important piece here is that the binding element put itself in the list of unhandled elements when creating the channel. An unhandled element is one that didn't get turned into a piece of the channel stack immediately during creation. The transport looks at this list of elements when searching for its message encoder. Remember, many transports have a default message encoding that they use if you don't specify one. If your custom message encoder fails to put itself in the list of elements, then it looks like the binding never contained a message encoder. Everything will appear to work because the transport used its default encoder, but the encoding specified in the binding is not getting called.
This is actually all the code you need for a custom message encoder. Later this week I'll put together some test programs using this encoder to demonstrate how it works.
Next time: One Month Until TechEd 2006
Now that we've seen how to write a custom transport, I thought I'd tackle another common request, which is to write a custom message encoder. A message encoder serializes an instance of the Message class into bytes. This is generally a much simpler operation than actually sending those bytes to someone, so we'll be able to get through the message encoder sample much faster than the transport sample.
I didn't actually want to come up with my own encoder since that would take a lot of work without showing you anything reusable. Instead, I decided to build a message encoder class that wraps an existing encoder and counts the number of bytes read and written while encoding. If you know how long your operation took, you can then figure out your throughput regardless of the transport you're using. This isn't necessarily going to be the exact figure of bytes across the network because most transports add framing and protocol information to the byte stream during transmission. However, this does tell you how many message bytes are being transmitted.
To do things in the opposite way as the last example, I'll build this encoder from the inside out. The piece that actually does the work is an instance of MessageEncoder, so let's build one of those.
using System; using System.IO; using System.ServiceModel.Channels; namespace CountingEncoder { class CountingEncoder : MessageEncoder { readonly CountingEncoderBindingElement bindingElement; readonly MessageEncoder innerEncoder; public CountingEncoder(CountingEncoderBindingElement bindingElement, MessageEncoder innerEncoder) { this.bindingElement = bindingElement; this.innerEncoder = innerEncoder; } public override string ContentType { get { return this.innerEncoder.ContentType; } } public override string MediaType { get { return this.innerEncoder.MediaType; } } public override MessageVersion MessageVersion { get { return this.innerEncoder.MessageVersion; } } public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager) { Message message = this.innerEncoder.ReadMessage(buffer, bufferManager); bindingElement.ReadCount++; bindingElement.ReadBytes += buffer.Count; return message; } public override Message ReadMessage(Stream stream, int maxSizeOfHeaders) { return this.innerEncoder.ReadMessage(new CountingStream(this.bindingElement, stream), maxSizeOfHeaders); } public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) { ArraySegment<byte> buffer = innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset); bindingElement.WriteCount++; bindingElement.WriteBytes += buffer.Count; return buffer; } public override void WriteMessage(Message message, Stream stream) { innerEncoder.WriteMessage(message, new CountingStream(this.bindingElement, stream)); } } }
All I've done here is wrap the methods of another message encoder and pass some counts up to the binding element for centralized record keeping. I created a custom stream class so that we can measure exactly how many operations and bytes are used while streaming, instead of just guessing based on the size of the stream.
using System.IO; namespace CountingEncoder { class CountingStream : Stream { readonly CountingEncoderBindingElement bindingElement; readonly Stream innerStream; public CountingStream(CountingEncoderBindingElement bindingElement, Stream innerStream) { this.bindingElement = bindingElement; this.innerStream = innerStream; } public override bool CanRead { get { return this.innerStream.CanRead; } } public override bool CanSeek { get { return this.innerStream.CanSeek; } } public override bool CanWrite { get { return this.innerStream.CanWrite; } } public override void Close() { this.innerStream.Close(); } public override void Flush() { this.innerStream.Flush(); } public override long Length { get { return this.innerStream.Length; } } public override long Position { get { return this.innerStream.Position; } set { this.innerStream.Position = value; } } public override int Read(byte[] buffer, int offset, int count) { int bytesRead = this.innerStream.Read(buffer, offset, count); this.bindingElement.ReadCount++; this.bindingElement.ReadBytes += bytesRead; return bytesRead; } public override long Seek(long offset, SeekOrigin origin) { return this.innerStream.Seek(offset, origin); } public override void SetLength(long value) { this.innerStream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) { this.innerStream.Write(buffer, offset, count); this.bindingElement.WriteCount++; this.bindingElement.WriteBytes += count; } } }
Like the message encoder, the stream simply wraps an inner stream that is doing all the work and passes the counts up to the binding element.
Next time: Building A Custom Message Encoder to Record Throughput, Part 2
Today's article is just a summary of what we've put together with the file transport and a demonstration of how it works. There's no new code left to show, although I'll go over a number of improvements we could make to the transport in the future. Here are the previous articles in this series:
Running the server creates a listener at my.file://localhost/x, which for the machine I'm running this on is going to resolve to c:\x. The listener creates a file system watcher to look for a request message at c:\x\request. We're using the TextMessageEncoder, so the format of the request message is expected to be a text-encoded message using SOAP.
Meanwhile, I've started up the client and given it an input string to send as the message body.
Creating factory... done. Creating channel... done. Opening channel... done. Enter some text (Ctrl-Z to quit): asdf Sending request... done. Processing reply: http://reflection Reply: fdsa Enter some text (Ctrl-Z to quit): ^Z
What this is going to look like on the "wire" is first a request message with the http://reflect action written to c:\x\request:
<s:Envelope xmlns:s=http://www.w3.org/2003/05/soap-envelope xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">http://reflect</a:Action> <a:To s:mustUnderstand="1">my.file://localhost/x</a:To> </s:Header> <s:Body> <string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">asdf</string> </s:Body> </s:Envelope>
Then, the server writes back a reply message with the http://reflection action to c:\x\reply:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">http://reflection</a:Action> <a:To s:mustUnderstand="1">my.file://localhost/x</a:To> </s:Header> <s:Body> <string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">fdsa</string> </s:Body> </s:Envelope>
This transport is fully operational, but it's taken a number of shortcuts to get to that point in the smallest amount of code. Here's what has been left behind:
I hope this series has helped explain the process of writing a transport. It's been tough having a long series like this. I hope in the future we can make the process of building a transport easier. There will be other transport examples in the SDK to look at when the product is released.
The next series is going to be a much shorter one and covers writing a custom message encoder.
Next time: Building A Custom Message Encoder to Record Throughput, Part 1
Impersonation and delegation are remote security concepts that really seem to throw people for a loop. That may be because every single networking implementation seems to treat them differently. Transport security is no exception, and this article covers the unique characteristics you need to know. Configuring impersonation in the service and picking certificates is exactly the same between transport and message security so those topics aren't included here.
Impersonation is the ability of a server application to take on the identity of the client. It is common for services to use impersonation when validating access to resources. The server application runs using a service account, but when the server accepts a client connection, it impersonates the client so that access checks are performed using the client's credentials. Transport security is a mechanism both for passing credentials and securing communication using those credentials. This document describes using transport security in Windows Communication Foundation (WCF) with the impersonation feature.
There are five levels of impersonation that are used with transport security.
Impersonation Level
None
The server application does not attempt to impersonate the client.
The server application can perform access checks against the client's credentials, but does not receive any information about the client's identity. This impersonation level is only meaningful for on-machine communication, such as named pipes. Using Anonymous with a remote connection promotes the impersonation level to Identify.
Identify
The server application knows about the client's identity and can perform access validation against the client's credentials, but cannot impersonate the client. Identify is the default impersonation level used with SSPI credentials in WCF unless the token provider provides a different impersonation level.
Impersonate
The server application can access resources on the server machine as the client in addition to performing access checks. The server application cannot access resources on remote machines using the client's identity because the impersonated token does not have network credentials.
Delegate
In addition to having the power of Impersonate, the server application can access resources on remote machines using the client's identity and pass the identity to other applications. The server domain account must be marked as trusted for delegation on the domain controller to use these additional powers. This level of impersonation cannot be used with client domain accounts marked as sensitive.
The impersonation levels most commonly used with transport security are Identify and Impersonate. The None and Anonymous impersonation levels are not recommended for typical use and many transports do not support using those levels with authentication. The Delegate impersonation level is a powerful feature that should be used with care. Only trusted server applications should be given the permission to delegate credentials.
Using impersonation at the Impersonate or Delegate levels requires the server application to have the SeImpersonatePrivilege privilege. An application has this privilege by default if it is running on an account in the Administrators group or on an account with a Service SID (Network Service, Local Service, or Local System). Impersonation does not require mutual authentication of the client and server. Some authentication schemes that support impersonation, such as NTLM, cannot be used with mutual authentication.
The choice of a transport in WCF affects the possible choices for impersonation. This document covers the standard HTTP, TCP, and named pipe transports in WCF. Custom transports have their own restrictions on support for impersonation.
The named pipe transport is only intended for use on the local machine. Named pipes in WCF explicitly disallow cross-machine connections. Named pipes cannot be used with impersonation levels Impersonate or Delegate. The named pipe cannot enforce the on-machine guarantee at these impersonation levels.
The HTTP transport supports several schemes for authentication, see the HTTP Authentication document for more details. The supported level of impersonation varies depending on the authentication scheme. The Anonymous authentication scheme ignores impersonation. The Basic authentication scheme only supports the Delegate impersonation level. All lower impersonation levels will be upgraded. The Digest authentication scheme only supports the Impersonate and Delegate impersonation levels. The Ntlm authentication scheme, either directly or through Negotiate, cannot be used with the Delegate impersonation level except on the local machine. The Negotiate authentication scheme leading to Kerberos can be used with any supported impersonation level.
Next time: Building a Custom File Transport, Part 11: Putting it Together
Today is the final day of coding for the file transport. The last piece is the request context for the server. A request context forces the correlation between client and server messages in the request-reply message pattern. One thing that differentiates creating a request context from the other pieces we've looked at is that there is no RequestContext base class. You have to directly implement the IRequestContext interface, which means there's a little more work here.
using System; using System.ServiceModel; using System.ServiceModel.Channels; namespace FileTransport { partial class FileReplyChannel { class FileRequestContext : IRequestContext { bool aborted; readonly Message message; readonly FileReplyChannel parent; CommunicationState state; readonly object thisLock; readonly object writeLock; public FileRequestContext(Message message, FileReplyChannel parent) { this.aborted = false; this.message = message; this.parent = parent; this.state = CommunicationState.Opened; this.thisLock = new object(); this.writeLock = new object(); } public void Abort() { lock (thisLock) { if (this.aborted) { return; } this.aborted = true; this.state = CommunicationState.Faulted; } } public IAsyncResult BeginReply(Message message, TimeSpan timeout, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); } public IAsyncResult BeginReply(Message message, AsyncCallback callback, object state) { return BeginReply(message, this.parent.DefaultSendTimeout, callback, state); } public void Close(TimeSpan timeout) { lock (thisLock) { this.state = CommunicationState.Closed; } } public void Close() { Close(this.parent.DefaultCloseTimeout); } public void EndReply(IAsyncResult result) { throw new Exception("The method or operation is not implemented."); } public void Reply(Message message, TimeSpan timeout) { lock (thisLock) { if (this.aborted) { throw new CommunicationObjectAbortedException(); } if (this.state == CommunicationState.Faulted) { throw new CommunicationObjectFaultedException(); } if (this.state == CommunicationState.Closed) { throw new ObjectDisposedException("this"); } } this.parent.ThrowIfDisposedOrNotOpen(); lock (writeLock) { this.parent.WriteMessage(FileChannelBase.PathToFile(this.parent.LocalAddress.Uri, "reply"), message); } } public void Reply(Message message) { Reply(message, this.parent.DefaultSendTimeout); } public Message RequestMessage { get { return message; } } public void Dispose() { Close(); } } } }
Probably the biggest additional work item is the need to manually track the state of the request context. This state machine tracking is what you normally get for free from a CommunicationObject. Like most of the other classes in the example, I've kept this pretty simple to reduce the size of the example.
Otherwise, the implementation of the request context is straightforward. The goal here is to supply something that looks a lot like a channel for the reply to be sent through. I've embedded the request context as a partial class of the parent channel although that is entirely optional. You can design the request context to be independent of its channel if you want.
Although we're done with the code and you can run both the client and server now, there's one more article in this series coming next week to wrap things up. This will talk about what you'd have to do to make this simple file transport ready for production use.
Next time: Using Impersonation with Transport Security
The second part of the server code to fill in is the reply channel. After today, we'll just have one more piece left to go. That means tomorrow, the code for this sample will be done. An eleventh and final chapter of this series will appear next week to talk about some results from actually running the sample.
The reply channel lies in wait for the client to write a request message. Like the request channel, the reply channel in this sample uses a file system watcher to detect when it needs to grab the file. When a file appears in the right location, the channel will suck out the contents of the file and reconstitute it as a SOAP message. This message gets handed off to build a request context, where the actual work of performing the reply takes place.
using System; using System.IO; using System.ServiceModel; using System.ServiceModel.Channels; namespace FileTransport { partial class FileReplyChannel : FileChannelBase, IReplyChannel { readonly EndpointAddress localAddress; readonly object readLock; public FileReplyChannel(BufferManager bufferManager, MessageEncoderFactory encoderFactory, EndpointAddress address, FileReplyChannelListener parent) : base(bufferManager, encoderFactory, address, parent, parent.Streamed, parent.MaxReceivedMessageSize) { this.localAddress = address; this.readLock = new object(); } public IAsyncResult BeginReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); } public IAsyncResult BeginReceiveRequest(AsyncCallback callback, object state) { return BeginReceiveRequest(DefaultReceiveTimeout, callback, state); } public IAsyncResult BeginTryReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); } public IAsyncResult BeginWaitForRequest(TimeSpan timeout, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); } public IRequestContext EndReceiveRequest(IAsyncResult result) { throw new Exception("The method or operation is not implemented."); } public bool EndTryReceiveRequest(IAsyncResult result, out IRequestContext context) { throw new Exception("The method or operation is not implemented."); } public bool EndWaitForRequest(IAsyncResult result) { throw new Exception("The method or operation is not implemented."); } public EndpointAddress LocalAddress { get { return this.localAddress; } } public IRequestContext ReceiveRequest(TimeSpan timeout) { ThrowIfDisposedOrNotOpen(); lock (readLock) { Message message = ReadMessage(PathToFile(LocalAddress.Uri, "request")); return new FileRequestContext(message, this); } } public IRequestContext ReceiveRequest() { return ReceiveRequest(DefaultReceiveTimeout); } public bool TryReceiveRequest(TimeSpan timeout, out IRequestContext context) { context = null; bool complete = WaitForRequest(timeout); if (!complete) { return false; } context = ReceiveRequest(DefaultReceiveTimeout); return true; } public bool WaitForRequest(TimeSpan timeout) { ThrowIfDisposedOrNotOpen(); try { File.Delete(PathToFile(LocalAddress.Uri, "request")); using (FileSystemWatcher watcher = new FileSystemWatcher(LocalAddress.Uri.AbsolutePath, "request")) { watcher.EnableRaisingEvents = true; WaitForChangedResult result = watcher.WaitForChanged(WatcherChangeTypes.Changed, (int)timeout.TotalMilliseconds); return !result.TimedOut; } } catch (IOException exception) { throw ConvertException(exception); } } } }
Bogus code alert: The TryReceiveRequest method here deserves particular scorn for being the most egregiously bad code in the entire sample. Notice the especially bad use of timeouts here. Notice the awful race condition that would occur when multiple clients and servers attempted to use the same file URL. See how many scenarios you can work out to attack either the client or server through just this bad behavior.
Next time: Building a Custom File Transport, Part 10: Request Context
The client is actually done at this point. I don't know how many people have actually tried running the code or if you're all just following along. If you did try running the client, you would see a file appear on your hard drive, probably at c:\x\request. Inside that file would be a SOAP message representing the client request. Fabulous. Now we just need to write the rest of the server so that something actually happens in response to that message.
The server has three classes left to write. We'll need the channel listener, which corresponds to the channel factory on the client side. We'll need the reply channel, which corresponds to the request channel on the client side. Finally, we'll need the request context. That piece is entirely unique to the server and is what correlates request messages with replies.
First up is the channel listener. This class is remarkably similar to the channel factory except for the methods that allow you to asynchronously receive channels back from the listener. Since the example doesn't support asynchronous operations, that means the listener and factory are pretty much identical in terms of code.
using System; using System.IO; using System.ServiceModel; using System.ServiceModel.Channels; namespace FileTransport { class FileReplyChannelListener : ChannelListenerBase<IReplyChannel> { readonly BufferManager bufferManager; readonly MessageEncoderFactory encoderFactory; public readonly long MaxReceivedMessageSize; readonly string scheme; public readonly bool Streamed; readonly Uri uri; public FileReplyChannelListener(FileTransportBindingElement transportElement, BindingContext context) : base(context.Binding) { MessageEncodingBindingElement messageEncodingElement = context.UnhandledBindingElements.Remove<MessageEncodingBindingElement>(); this.bufferManager = BufferManager.CreateBufferManager(transportElement.MaxBufferPoolSize, int.MaxValue); this.encoderFactory = messageEncodingElement.CreateMessageEncoderFactory(); MaxReceivedMessageSize = transportElement.MaxReceivedMessageSize; this.scheme = transportElement.Scheme; Streamed = transportElement.Streamed; this.uri = new Uri(context.ListenUriBaseAddress, context.ListenUriRelativeAddress); } protected override void OnOpen(TimeSpan timeout) { base.OnOpen(timeout); Directory.CreateDirectory(Uri.AbsolutePath); } protected override IReplyChannel OnAcceptChannel(TimeSpan timeout) { EndpointAddress address = new EndpointAddress(Uri); return new FileReplyChannel(this.bufferManager, this.encoderFactory, address, this); } protected override IAsyncResult OnBeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); } protected override IReplyChannel OnEndAcceptChannel(IAsyncResult result) { throw new Exception("The method or operation is not implemented."); } protected override IAsyncResult OnBeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); } protected override bool OnEndWaitForChannel(IAsyncResult result) { throw new Exception("The method or operation is not implemented."); } protected override bool OnWaitForChannel(TimeSpan timeout) { throw new Exception("The method or operation is not implemented."); } public override Uri Uri { get { return this.uri; } } public override MessageVersion MessageVersion { get { return MessageVersion.Default; } } public override string Scheme { get { return this.scheme; } } } }
Next time: Building a Custom File Transport, Part 9: Reply Channel
I've been providing the contents for this article in bits and pieces but now the whole thing is assembled together. If you've been wondering how to make your WCF application safer for NATs and firewalls, this should help you out.
The client and server side of a network connection frequently do not have a direct and open path for communication. Packets are filtered, routed, analyzed, and transformed both on the endpoint machines and by intermediate machines on the network. Network Address Translators (NATs) and firewalls are common examples of intermediate applications that can participate network communication.
Windows Communication Foundation (WCF) transports and message exchange patterns react differently to the presence of NATs and firewalls. This document describes how NATs and firewalls function in common network topologies. Recommendations for specific combinations of WCF transports and message exchange patterns are given that help make your applications more robust to NATs and firewalls on the network.
Network address translation was created to enable several machines to share a single external IP address. A port-remapping NAT maps an internal IP address and port for a connection to an external IP address with a new port number. The new port number allows the NAT to correlate return traffic with the original communication. Many home users now have an IP address that is only privately routable and rely on a NAT to provide global routing of packets.
A NAT does not provide a security boundary. However, common NAT configurations prevent the internal machines from being directly addressed. This both protects the internal machines from some unwanted connections and makes it difficult to write server applications that need to asynchronously send data back to the client. The NAT rewrites the addresses in packets to make it seem like connections are originating at the NAT machine. This causes the server to fail when it attempts to open a connection back to the client. If the server uses the client's perceived address, it will fail because the client address is not publicly routable. If the server uses the NAT's address, it will fail to connect because no application is listening on that machine.
Some NATs support the configuration of forwarding rules to allow external machines to connect to a particular internal machine. The instructions for configuring forwarding rules varies between different NATs and asking end users to change their NAT configuration is not recommended for most applications. Many end users either cannot or do not want to change their NAT configuration for a particular application.
A firewall is a software or hardware device that applies rules to the traffic passing through to decide whether to allow or deny passage. Firewalls can be configured to examine incoming, outgoing, or both streams of traffic. The firewall provides a security boundary for the network at either the edge of the network or on the endpoint host. Business users have traditionally kept their servers behind a firewall to prevent malicious attacks. Since the introduction of the personal firewall in Windows XP Service Pack 2, the number of home users behind a firewall has greatly increased as well. This makes it very likely that one or both ends of a connection will have a firewall examining packets.
Firewalls vary greatly in terms of their complexity and capability for examining packets. Simple firewalls apply rules based on the source and destination addresses and ports in packets. Intelligent firewalls can also examine the contents of packets to make decisions. These firewalls come in many different configuration and are often used for specialized applications.
A common configuration for a home user firewall is to prohibit incoming connections unless an outgoing connection was made to that machine previously. A common configuration for a business user firewall is to prohibit incoming connections on all ports except a group specifically identified. An example is a firewall that prohibits connections on all ports except for ports 80 and 443 to provide HTTP and HTTPS service. Managed firewalls exist for both home and business users that permit a trusted user or process on the machine to change the firewall configuration. Managed firewalls are more common for home users where there is not a corporate policy controlling network usage.
Teredo is an IPv6 transition technology that enables the direct addressability of machines behind a NAT. Teredo relies on the use of a public and globally routable server to advertise potential connections. The Teredo server gives the application client and server a common meeting point at which they can exchange connection information. The machines then request a temporary Teredo address and packets are tunneled through the existing network. Teredo support in WCF requires enabling IPv6 and Teredo support in the operating system. Teredo is supported by Windows XP and later operating system. Windows Vista and later operating systems support IPv6 by default and only require the user to enable Teredo. Windows XP SP2 and Windows Server 2003 require the user to enable both IPv6 and Teredo. See the Teredo Overview for Microsoft Windows for more information.
Selecting a transport and message exchange pattern is a three-step process.
1. Analyze the addressability of the endpoint machines. Enterprise servers commonly have direct addressability while end users commonly have their addressability blocked by NATs. If both endpoints are behind a NAT, such as in peer-to-peer scenarios between end users, then a technology like Teredo may be required to provide addressability.
2. Analyze the protocol and port restrictions of the endpoint machines. Enterprise servers are typically behind strong firewalls that block many ports. However, port 80 is frequently open to permit HTTP traffic and port 443 is open to permit HTTPS traffic. End users are less likely to have port restrictions but may be behind a firewall that only permits outgoing connections. Some firewalls permit management by applications on the endpoint to selectively open connections.
3. Compute the transports and message exchange patterns that are permitted by the addressability and port restrictions of the network.
A common topology for client-server applications is to have clients that are behind a NAT without Teredo with outbound-only firewall and a server that is directly addressable with a strong firewall. In this scenario, the TCP transport with duplex message exchange pattern and HTTP transport with request-reply message exchange pattern work well. A common topology for peer-to-peer applications is to have both endpoints behind NATs and firewalls. In this scenario, and in scenarios where the network topology is unknown, consider the following recommendations.
· Do not use dual transports. A dual transport opens more connections, which reduces the chance of connecting successfully.
· Do support establishing back-channels over the originating connection. Using back channels, such as in duplex TCP, opens fewer connections, which increases the chance of connecting successfully.
· Do employ a reachable service for either registering endpoints or relaying traffic. Using a globally reachable connection service, such as a Teredo server, greatly increases the chance of connecting successfully when the network topology is restrictive or unknown.
The following tables examine the One Way, Request-Reply, and Duplex message exchange patterns, and the standard TCP, TCP with Teredo, and standard and dual HTTP transports in WCF.
Addressability
Server Direct
Server Direct with NAT traversal
Server NAT
Server NAT with NAT traversal
Client Direct
Any transport and MEP
Not supported
Client Direct with NAT traversal
TCP with Teredo and any MEP3
Client NAT
Any non-dual transport and MEP1
Client NAT with NAT traversal
All but dual HTTP and any MEP123
1: Duplex MEP requires TCP transport
2: Dual TCP transport requires Teredo
3: Windows Vista has a machine-wide configuration option to support HTTP with Teredo
Firewall Restrictions
Server Open
Server with Managed Firewall
Server with HTTP Only Firewall
Server with Outbound Only Firewall
Client Open
Any HTTP transport and MEP
Client with Managed Firewall
Client with HTTP Only Firewall
Client with Outbound Only Firewall
Any HTTP transport and any non-duplex MEP
Next time: Building a Custom File Transport, Part 8: Channel Listener