Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can XmlDictionaryReader really handle binary XML? If not, what does?

Tags:

soap

xml

binary

wcf

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">
  <s:Header>
     <a:Action s:mustUnderstand="1">http://tempuri.org/IService1/Echo</a:Action>
     <a:MessageID>urn:uuid:21a33e81-bfab-424f-a2e5-5116101a7319</a:MessageID>
     <a:ReplyTo>
        <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
     </a:ReplyTo>
     <a:To s:mustUnderstand="1">http://ipv4.fiddler:25381/Service1.svc</a:To>
  </s:Header>

  <s:Body>
      <Echo xmlns="http://tempuri.org/">
          <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?

like image 307
Richard Berg Avatar asked Apr 07 '26 10:04

Richard Berg


2 Answers

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(PROT.10).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

like image 80
Richard Berg Avatar answered Apr 10 '26 02:04

Richard Berg


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:

  • Writing a WCF message inspector
  • Building an HTTP User Agent Message Inspector
  • WCF Client Message Inspector

Marc

like image 29
marc_s Avatar answered Apr 10 '26 02:04

marc_s



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!