tags:

views:

664

answers:

3

I'm trying to write a debugging tool that allows the user to view WCF's new binary XML format (application/soap +msbin1) in plain text. Once I found the XmlDictionaryReader class I thought I'd be done in minutes, but it's not working as expected.

private string DecodeBinaryXML(byte[] binaryBuffer)
{
    if (binaryBuffer == null)
    {
        return "";
    }

    try
    {
        var doc = new XmlDocument();
        using (var binaryReader = XmlDictionaryReader.CreateBinaryReader(binaryBuffer, XmlDictionaryReaderQuotas.Max))
        {                    
            doc.Load(binaryReader);
            binaryReader.Close();
        }

        var textBuffer = new StringBuilder();
        var settings = new XmlWriterSettings()
        {
            // lots of code not relevant to the question
        };
        using (var writer = XmlWriter.Create(textBuffer, settings))
        {
            doc.Save(writer);
            writer.Close();
        }

        return textBuffer.ToString();
    }
    catch (Exception ex)
    {
        // just display errors in the text viewer
        return ex.ToString();
    }
}

Every single "soap+msbin1" sample I've found online or generated myself throws a parse exception at doc.Load().

To see what was going on, I created a simple test app and attacked the problem from the other direction.

// client
static void Main(string[] args)
{
    var binding = new CustomBinding(new TextMessageEncodingBindingElement(), 
                                    new HttpTransportBindingElement());            
    var proxy = ChannelFactory<IService1>.CreateChannel(binding, 
               new EndpointAddress("http://ipv4.fiddler:25381/Service1.svc"));
    Console.WriteLine(proxy.Echo("asdf"));
}

// shared interface
[ServiceContract()]
public interface IService1
{
    [OperationContract]
    string Echo(string input);
}

// server
public class Service1 : IService1
{
    public string Echo(string input)
    {
        return "WCF says hi to: " + input;
    }
}

Running it kicks off an http request that looks like:

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" 
            xmlns:a="http://www.w3.org/2005/08/addressing"&gt;
  <s:Header>
     <a:Action s:mustUnderstand="1">http://tempuri.org/IService1/Echo&lt;/a:Action&gt;
     <a:MessageID>urn:uuid:21a33e81-bfab-424f-a2e5-5116101a7319</a:MessageID>
     <a:ReplyTo>
        <a:Address>http://www.w3.org/2005/08/addressing/anonymous&lt;/a:Address&gt;
     </a:ReplyTo>
     <a:To s:mustUnderstand="1">http://ipv4.fiddler:25381/Service1.svc&lt;/a:To&gt;
  </s:Header>

  <s:Body>
      <Echo xmlns="http://tempuri.org/"&gt;
          <input>asdf</input>
      </Echo>
  </s:Body>
</s:Envelope>

I converted this XML into binary two different ways. First, using XmlDictionaryWriter:

$fs = [system.io.file]::Create("c:\temp\soap.bin")
$writer = [system.xml.xmldictionarywriter]::CreateBinaryWriter($fs)
$xml = [xml] (gc C:\temp\soap.xml)
$xml.Save($writer)
$writer.Close(); $fs.Close()

Then, using WCF and the same network sniffer:

    @@ -1,7 +1,7 @@
     // client
     static void Main(string[] args)
     {
-        var binding = new CustomBinding(new TextMessageEncodingBindingElement(), 
+        var binding = new CustomBinding(new BinaryMessageEncodingBindingElement(), 
                                         new HttpTransportBindingElement());

Method #1 gave 397 bytes of binary-looking gunk. Method #2 shows 169 bytes of very different binary gunk. Apart from a few strings that appear in both outputs, I don't see a lot of similarity in the two encodings. No wonder, then, that XmlDictionaryReader can't understand the output of a WCF service!

Is there a secret to decoding this format, or am I on the wrong path entirely?

+1  A: 

Binary gunk..... well, you're using the BinaryEncoding!

var binding = new CustomBinding(new BinaryMessageEncodingBindingElement(), 
                                new HttpTransportBindingElement());

Can you - just for argument's sake - try to use the TextEncoding instead and see if that works?? Also - by default, WCF will encrypt and sign every message, to if you trap the wire, you should only see binary garbage! :-)

Also, at what point in the WCF communication are you trying to intercept these messages?

If you intercept them "on the wire" between the client and the server, they'll be binary encoded in your setup - you'll get gooblydeguck.

However, WCF offers a great extensibility story, so you could capture the messages before they're binary encoded (on the client), or after they've been decoded (on the server, incoming). Check into Message Inspectors - they allow you to have a look at the messages travelling through the WCF stack as they're being built on the client and unpacked on the server!

See some excellent resources:

Marc

marc_s
"Gunk" is figurative -- I used a hex editor of course :) I'm intercepting them "on the wire" (well, on the http proxy). Having it be binary is expected. What I didn't expect is for the XmlDictionary* classes to use a DIFFERENT binary encoding than the WCF classes; I thought one was built on the other. I will investigate the links, but I'm really hoping for a tool that works "on the wire" without client or server intervention.
Richard Berg
Well, as I said - by default (unless you specifically turn it off yourself), the data on the wire for WCF is encrypted and signed - not sure if you can easily get around that :-(
marc_s
Really? The most widely used binding (basicHttpBinding) definitely doesn't encrypt; I sniff it all the time, hence the desired to reuse those sniffer tools. I'm 90% sure the binding defined by my code above doesn't turn on encryption either. If you have documentation on this I'd love to see it!
Richard Berg
A: 

Besides the answer given by marc_s, keep in mind that XmlDictionaryReader is just an abstract class extending the interface for XmlReader (same applies to XmlDictionaryWriter). They are still dealing purely in terms of the InfoSet and not any specific representation of it.

In terms of actually reading/writing the binary xml format used by the BinaryMessageEncoder, that's done by two internal classes implemented by WCF: XmlBinaryReader and XmlBinaryWriter. I guess you could use them directly if you can use some reflection around, but other than that, they are really meant to be used indirectly through the BinaryMessageEncoder.

BTW, you can certainly use the encoder directly as I show in this blog post.

tomasr
I am using the XmlBinaryReader/Writer classes. Let me clarify the question with more detail.
Richard Berg
The blog post is helpful. Using encoder as you describe gives output closer to "method #2." Is there a corresponding decoder? Closest thing I can find is mebe.GetProperty<T>. That method has two pitfalls: (a) have to create a whole BindingContext (b) have to know the strong type of the data elements. I don't mind doing (a) if I have to [though it makes the tool less generally applicable than I thought]. But (b) seems like a showstopper.
Richard Berg
The MessageEncoder classes in WCF do both encoding and decoding. You can read a messaging using it as well using the ReadMessage() methods().
tomasr
Played with this some more. It's close to what I'm looking for. Unfortunately, every way I've found to consume MessageEncoder has some point in the stack where I can't proceed without knowing the strong type of the data in advance (a catch-22).
Richard Berg
+1  A: 

Got a promising response from Carlos Figueira @ MS.

WCF uses a "static dictionary" which encode some well-known strings into (small) ids. For example, the strings "Envelope", "http://www.w3.org/2003/05/soap-envelope", "http://www.w3.org/2005/08/addressing" and so on are represented only as a few bytes. So to be able to parse the requests that are sent by WCF, you need to pass that dictionary (IXmlDictionary) to the XmlDictionaryReader.CreateBinaryReader method.

The whole dictionary is documented at http://msdn.microsoft.com/en-us/library/cc219175%28PROT.10%29.aspx. The code to read the request should look something like:

public class Post_e9208540_7877_4318_909d_92eb8490ab58
{
    static XmlDictionary dictionary;
    static XmlDictionary GetDictionary()
    {
        if (dictionary == null)
        {
            XmlDictionary temp = new XmlDictionary();
            dictionary = temp;
            temp.Add("mustUnderstand");
            temp.Add("Envelope");
            temp.Add("http://www.w3.org/2003/05/soap-envelope");
            temp.Add("http://www.w3.org/2005/08/addressing");
            ...
        }
        return dictionary;
    }
    public static void DecodeBinaryMessage(byte[] message)
    {
        XmlDictionaryReader reader = XmlDictionaryReader.CreateBinaryReader(message, 0, message.Length, GetDictionary(), XmlDictionaryReaderQuotas.Max);
        Console.WriteLine(reader.ReadOuterXml());
    }
}

I'll update this answer with more details if it leads to a working solution.

edit: yup, works like a charm! Only problem with Carlos' solution is that ReadOuterXml() doesn't seem to work. Reading into an XmlDocument and then writing out a Stream allows much better control of the formatting anyway, so that's what I stuck with.

Note: replicating the dictionary in the MS spec takes about 500 lines of code. I'd recommend copying mine unless you're a masochist - http://tfstoys.codeplex.com/sourcecontrol/changeset/view/26191?projectName=tfstoys#499486

Richard Berg