/cfWSE

comment(s) 

CF .NET (RTM in Everett Final Beta) + WSE (WSDK Released)

http://www.brains-N-brawn.com/cfWSE 1/1/2003 casey chesnut

This article details my experience working with the WSE, and my initial attempts / thoughts on using the 2nd gen WS protocols from within the limits of CF .NET

.NET is about web services and mobile devices ... and I'm sold. Everywhere, if I see somebody writing something down, I'm like "they should be using a Pocket PC". Went to a dog show ... judges should have been using a PPC (NOTE there are alot of weird people at dog shows, myself included). Went to an amateur car race ... wrote a quick prototype PPC app for pit crews (to impress the umbrella girls ... it didn't work). V2 could be made so sweet with a GPS device, and a wireless connection (e.g./noSink). And these wireless connections are rapidly coming available through 3G phones and Wi-Fi hotspots, with Pocket PC Phone Edition and SmartPhone devices poised to deliver the mobile computing power. Web Services are evolving as well. The web service stack (XML, SOAP, XSD, WSDL, UDDI) continually move to better standardization and interoperability. While 2nd gen GXA WS specs are being worked on and even have released implementations like the WSE (previously WSDK) to support more complex applications. Can you say convergence ...

Timeline

The WSDK (Web Service Development Kit) was released late August 2002. At that time it was a Technical Preview (aka Alpha) and now it is renamed / released / and supported as the WSE (Web Service Enhancements ... would have preferred 'Augmentation') December 2002. It has a lower level API for building SOAP messages on clients and a higher level API for use in ASP .NET Web Services. It does not explicitly work with .NET Remoting Web Services, but from a newsgroup posting it looks like that has been attempted with some success using the lower level API. The protocols it supports are WS-Routing (WS-Referral), WS-Security, DIME (WS-Attachments). In the future it will likely support WS-Inspection, WS-Coordination, WS-Transaction, BPEL4WS. From the newsgroup, I also saw mention of a WS-Authorization? And what happened to WS-License? Just guessing that the WSE will be included with VS .NET 2003. Prerequisite article is /wsdk which concentrates on the server side WSE web services. The programming model for the WSE is basically the same as the WSDK

The CF .NET (Compact Framework) was available as a tech preview late 2001. Beta 1 was available early to mid 2002. There was an update to the WebService components of the CF B1, made available from the CF team with a MapPoint sample on GotDotNet to fix some early bugs for B1. The CF is now in Beta 2 (MapPoint update no longer needed) with the Everett Beta 1. From an earlier attempt to play with the CF B2, I found the emulator painfully slow to work with. The Everett Final Beta (late 2002) emulator is much faster, and will be used in this article. Finally, the CF .NET went RTM for the Everrett Final Beta and will be included with VS .NET 2003. Previous article detailing CF .NET with 1st gen web service stack was  /noMadMap . Still waiting for some CF .NET books to hit the shelves, other than the Wrox early adopter

The WSE comes with a bunch of sample services (~10). Each service also has a console client application written using the full .NET Framework. That is great, but remember .NET is about web services AND mobile devices ... AND mobile devices do NOT have the full framework. Thus, to consume these gen 2 services using the SDE (Smart Device Extensions) we have to do a little more work

Microsoft.Web.Services.dll

Do not try this at home (serious; this was fun to try with the CF .NET, but ended up helping very little in the end). The WSE sample clients add a reference to the Microsoft.Web.Services.dll assembly. This assembly is used both by clients and web services. It is made to work with the full .NET Framework, but its worth a shot to resuse some of its existing codebase. Creating a new C# Smart Device Application project in VS .NET for the Pocket PC, and then adding a reference to the assembly gets you this warning dialog


took me way to long to add this image to this HTML page using VS .NET 2003, it lost a menu option I used to use alot

Of course we know that dialog is there just to scare off more timid developers, so press OK. Do a build and you get some more warnings about other assemblies that this one references

System.Configuration.Install ... who cares. System.Web should not be needed by the client? NOTE System.Web.Security does exist on CF .NET, so go ahead and manually add that reference. The lack of System.Security is probably going to be trouble! Quick test to create an object defined in that assembly and everything checks out. Get intellisense and the whole bit. NOTE tried this in CF .NET B1 as well, the reference could be added but intellisense did not work and the project would not build with a type from that assembly declared. Not sure which group (VS .NET, CF .NET, WSE) we thank for this improvement? I tried instantiating a couple different objects ... some worked, and others threw a TypeLoadException (get used to it). Also thought about creating empty dummy SDE PPC class libraries for the 3 assemblies missing above and then referencing them from my project to see if that helped the TypeLoadException. Checking Microsoft.Web.Services.dll metadata with ildasm it was pointed to strong named assemblies, so I could not get the pubkeytoken insterted in my dummy libs. Might have been able to disassemble Microsoft.Web.Services.dll, made those references less strict, and then reassemble it pointing to my dummy libs ... but I didn't try

Deploy to my actual PPC device and nothing happened. Ends up I had to uninstall the CF .NET Beta 1 and then it worked (Start-Settings-System-Memory-Remove programs). So far so good, now its time for some real tests

SumService

Before getting in too deep, wanted to make sure that the basic addition web service worked (e.g. the Hello World of the WS world). Added a web reference to WseQuickStart/SumService/SumService.asmx (not WSE enhanced). The oddities are that it created 2 objects for me: AddNumbers and AddNumbersWse. AddNumbers is the proxy that we are used to. One change from CF B1 is that it now includes the much needed Timeout property. Looking at the auto-gen'd code AddNumbersWse interits from WebServicesClientProtocol in the Microsoft.Web.Services assembly, instead of SoapHttpClientProtocol in System.Web.Services. This proxy mod had to be done manually when it was the WSDK. Nice. Instantiated a regular AddNumbers and called the AddInt() WebMethod on it. Got back a WebException. From CF B1 this was sometimes a localhost vs 127.0.0.1 problem. Had added the Web Reference as localhost, so I switched to 127.0.0.1. Still got a WebException. Quick look at the beta newsgroup and it recommends adding the reference with machine name. Add it as NOTEBOOK, and it worked. This seems like a step backwards from CF B1. Also, my installation would not let me debug SDE clients calling locally running web services ... it would always timeout. The scenario was I had to kick off a debug session, then kill it, then browse to the SDE executable on the emulator and execute it manually ... then it would work. If I did 'start without debugging' it would timeout as well. When it complained about not being able to deploy to the emulator a soft reset would work, and when that did not work a hard reset always did it. Kind of painful ... ended up working much better with web services deployed to other machines. NOTE At some point I got a String Resources message (or something like that) on my PPC which was fixed by adding a reference to System.SR.dll in my project. Adding this resource provides more detailed exception messages

SumService is just a standard WebService (non WSE). Now we get to the good stuff, consuming WSE enabled web services. We'll start with WS-Routing (easy), DIME (medium), and then finish up with WS-Security (difficult). I'll actually implement as much as I can, but will probably trail off towards the end because im doing this for free

WS-Routing

To call a routed WebService you must add a SoapHeader that looks something like this

<wsrp:path soap:actor="http://schemas.xmlsoap.org/soap/actor/next" soap:mustUnderstand="1" xmlns:wsrp="http://schemas.xmlsoap.org/rp">
<wsrp:action>http://microsoft.com/wse/samples/SumService/AddInt</wsrp:action>
<wsrp:to>http://notebook/WSEQuickStart/router/sumservice.asmx</wsrp:to>
<wsrp:id>uuid:bd360a63-4d38-461f-97b1-2f5f80ba766e</wsrp:id>
</wsrp:path>

The WSE sample clients all follow the same pattern: A console application (AddClient.cs) inheriting from AppBase.cs and instantiating a modified wsdl.exe proxy class (AddNumbers.cs). Attempt 1 was to just add the proxy class (AddNumbers.cs) to my PPC app. Built fine, but at runtime it throws a TypeLoadException. My immediate test was then to directly instantiate a WebServicesClientProtocol object, the main modification to the autogen'd proxy, and it threw a TypeLoadException. So then I just ignored that proxy and decided to create one from sratch by adding a web reference. Giving the Router/sumservice.asmx URL to the web reference dialog resulted in the error message 'SOAP Message Expected'. Same when appending ?wsdl. Since it would not let me create the proxy from the router URL, then I just decided to use the SumService proxy created above (same interface) and just point its URL to the router URL. Did not try wsdl.exe directly, but hope VS .NET will be able to generate proxies from routed web service URLs in the future ... at least ?wsdl. Had to use AddNumbers since AddNumbersWse also threw a TypeLoadException. With AddNumbers pointing to the router, a SoapHeaderException was returned. Tried debugging from the PPC app and also turned on Tracing for the WSE WebService, but it kept complaining about a process locking my trace file; this is a step back from the WSDK. Punted and tried this sequence in a full .NET Framework WinForm to get more exception info. NOTE an MSDN WSE article mentions the configuration element <faultDetail>, this existed in the WSDK but not in the WSE and you will get an exception if you add it

Created a WinForm app and then added the Web Reference. Looking at the proxy namespace the AddNumbersWse class was missing. Deleted that proxy and added an assembly reference (non web) to Microsoft.Web.Services.dll, re-added the Web Reference, and the Wse class was now there. Regardless, I am just using the non-WSE proxy (AddNumbers) for this test and just changing its URL to the Router URL. This threw the same SoapHeaderException with the additional info 'Soap Header path was not understood'. Restarted my computer and tracing was still hosed. Input trace to the service was getting written, but not output. Checking the newsgroup, it might be that the ASP.NET worker process needed more permissions but I was already running as System. Switching over to AddNumbersWse and using tracing on the client; both output/input streams were written properly. Except this gave me a different SoapFault 'Endpoint not supported'. Looking at the referralCache.config, the endpoints are defined as 'localhost' which does not work for PPC, so I switched it to 'NOTEBOOK'. If referralCache.config is locked then do iisreset 1st. Ran it again and still got Endpoint not supported. Reason was my URI was WSEQuickStart/router/sumservice.asmx and not wsequickstart/router/sumservice.asmx. NOTE the docs say <r:exact> is supposed to be case-insensitive. Believe I pointed out this bug in my previous WSDK article. Once I got past the endpoint crap, the trace on my services output was finally written. So trace works on the output of the service side, unless there is some exception on the server side ... even though a SoapFault is returned. I think this is a bug too, the above SoapFaults should have been serialized to the trace output of the service. Alright, so it worked with AddNumbersWse after getting the endpoint stuff worked out, so I rolled back to AddNumbers. 'Soap Header path was not understood' again! Looking at the previous Traces, when a SoapFault was returned it includes a SoapHeader and body like this

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Header>
      <wsrp:path soap:actor="http://schemas.xmlsoap.org/soap/actor/next" soap:mustUnderstand="1" xmlns:wsrp="http://schemas.xmlsoap.org/rp">
        <wsrp:action>http://schemas.xmlsoap.org/soap/fault</wsrp:action>
        <wsrp:id>uuid:ef4218a8-3765-4efa-bb73-0f5555aee4b5</wsrp:id>
        <wsrp:fault>
          <wsrp:code>712</wsrp:code>
          <wsrp:reason>Endpoint Not Supported</wsrp:reason>
          <endpoint>http://notebook/WSEQuickStart/router/sumservice.asmx</endpoint>
        </wsrp:fault>
      </wsrp:path>
    </soap:Header>
    <soap:Body>
      <soap:Fault>
        <faultcode>soap:Client</faultcode>
        <faultstring>System.Web.Services.Protocols.SoapHeaderException: Endpoint Not Supported</faultstring>
        <faultactor>http://notebook/WSEQuickStart/router/sumservice.asmx</faultactor>
      </soap:Fault>
    </soap:Body>
  </soap:Envelope>

If you look real close you will see the irony. I can't get to the actual SoapFault because the SoapHeader has mustUnderstand=1; that exception is bubbling up from the proxy 1st and hiding the real SoapFault I want to see. Also note, the real SoapFault is not getting serialized because the output trace on the service is not getting written (bug detailed above) and now the client is not using the WSE class, so its client side trace functionality is not there to write out its input trace. This is loads of fun :) To see the real message I could use a proxy app to sniff the SOAP messages or use a SoapExtension to write out the trace. Opted for the SoapExtension and the obligatory Row Howard TraceExtension.cs. Had always registered SoapExtensions by using attributes (e.g. [TraceExtension()]) but had never done it using the configuration section like so (on the client app.config)

<configuration>
    <system.web>
        <webServices>
            <soapExtensionTypes>
                <add type="TraceExtension, WinFormWse" priority="1" group="0" />
                <!--add type="Microsoft.Web.Services.WebServicesExtension, Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="1" group="0" /-->
            </soapExtensionTypes>
        </webServices>
 </configuration>

Ends up it worked great and the resulting input to the client, so that I could see the real SoapFault, was: NOTE the technique above will not work on the PPC client because app.config's are not supported. An Attribute will have to be used. Also, CF B1 did not support SoapExtension's but RTM does; we will get to that later

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	<soap:Header>
		<wsrp:path soap:actor="http://schemas.xmlsoap.org/soap/actor/next" soap:mustUnderstand="1" xmlns:wsrp="http://schemas.xmlsoap.org/rp">
			<wsrp:action>http://schemas.xmlsoap.org/soap/fault</wsrp:action>
			<wsrp:id>uuid:82248eac-5a26-4931-bc1c-dd69a33a9a64</wsrp:id>
			<wsrp:fault>
			<wsrp:code>701</wsrp:code>
			<wsrp:reason>WS-Routing Header Required</wsrp:reason></wsrp:fault>
		</wsrp:path>
	</soap:Header>
	<soap:Body>
		<soap:Fault>
			<faultcode>soap:Client</faultcode>
			<faultstring>System.Web.Services.Protocols.SoapHeaderException: WS-Routing Header Required</faultstring>
			<faultactor>http://notebook/WSEQuickStart/router/sumservice.asmx</faultactor>
		</soap:Fault>
	</soap:Body>
</soap:Envelope>

Ok, so the real error is that the client must send the <wsrp:path> SoapHeader to the routed service. NOTE the <wsrp:path> SoapHeader is only returned form the service when an exception occurs. So my 1st thought is to turn this off on the server. No code to be changed since routing is all done through configuration files as expected. Looking at the config docs turned up nothing so I resorted to the spec, and found section 5.2.1.2. Suck, the header has to be sent by the client. The obvious options on the client end are a SoapExtension or a SoapHeader (maybe a WSE filter?)

SoapExtension

Curious about SoapExtensions support in CF RTM so tried that 1st on the PPC project. Started out by adding TraceExtension.cs and TraceExtensionAttribute.cs to the project and just applying [TraceExtension()] to the auto gen'd proxy. Would not build because I had the SoapServerMessage type in the code which does not exist in CF .NET. That was just being used to customize some output metadata, so I ripped that out. It built, and at runtime it gave me an InvalidDirectoryException because the file path was c:\log.txt as opposed to \Windows\log.txt for the PPC. Modified the attribute to read as [TraceExtension(Filename= '\\Windows\\log.txt')] and it worked. Both the input and output SoapMessages were written to log.txt on the file system of the PPC Emulator. NOTE when you declare the SoapExtension in app.config or web.config (as opposed to an Attribute) then GetInitializer()  is called on the WebService. At least on the PPC it was not getting called and the ability to declare it through a config file does not exist as stated above. GetInitializer is where I was previously setting the filename, as opposed to using an Attribute, and it was getting called from the full .NET Framework WinForm app with it declared as a <soapExtensionType> in the app.config. Also, note that Initialize() was getting called on the SoapExtension, with object parameter null, so I moved the filepath code there for the PPC. Now that we know SoapExtensions work on the PPC, it would just be a matter of hooking case SoapMessageStage.AfterSerialize: in ProcessMessage(), sucking the stream into and XmlDocument and then adding the appropriate SoapHeader above. Brute force methodology. NOTE I started to write the code this way but was getting an error when setting XmlNode.InnerXml with namespaces on XmlAttributes (e.g. soap:actor). This might be a bug although I didnt mess with it very long. One thing I thought of that I didnt try was appending the XmlNode to the XmlDoc before setting InnerXml, although this seems unnecessary since the XmlNode was created from the XmlDoc so it should have the proper namespaces already? Regardless, the solution was ugly so I set it aside

SoapHeader

A more elegant solution is to use a SoapHeader. Tried using the Microsoft.Web.Services.Routing.Path object 1st. It derives from System.Web.Services.Protocols.SoapHeader and has everything already defined. Except, it has no default constructor so you cannot use it directly. If it had a default constructor it would be as simple as adding one as a member to the generated proxy, decorating the WebMethod with a SoapHeaderAttribute; then in the client code creating an instance of the header on the proxy and finally setting its properties. Did not see an intuitive way to get one indirectly created either. Oh well, I had to write my own

//add public member to the auto gen'd proxy
//public MyPathHeader p;
//add to WebMethod that will be called on auto gen'd proxy
//[SoapHeaderAttribute("p")]

using System;
using System.Xml.Serialization;
using System.Web.Services.Protocols;

[XmlRoot(Namespace="http://schemas.xmlsoap.org/rp", ElementName="path")]
public class MyPathHeader : SoapHeader
{
	public MyPathHeader()
	{
		//from SoapHeader
		Actor = "http://schemas.xmlsoap.org/soap/actor/next";
		MustUnderstand = true;

		action = "http://microsoft.com/wse/samples/SumService/AddInt";
		to = "http://notebook/WSEQuickStart/router/sumservice.asmx";
		//id = "uuid:bd360a63-4d38-461f-97b1-2f5f80ba766e";
		id = "uuid:" + MyUtil.CreateGuid("D");
	}

	[XmlElement(Namespace="http://schemas.xmlsoap.org/rp")]
	public string action;
	[XmlElement(Namespace="http://schemas.xmlsoap.org/rp")]
	public string to;
	[XmlElement(Namespace="http://schemas.xmlsoap.org/rp")]
	public string id;
}

The real slick part is this works in CF .NET. The only thing missing that I wanted was the static method Guid.NewGuid(). Luckily, the public CF newsgroup has C# code posted to create a new guid, because each method call should be uniquely identifiable. Only extension I did to that code was to let it take a formatting string. An alternative method is to use SQL CE to gen a GUID. Now the client code looks like this to call the web service:

//note this is not the Wse proxy class
SumWS.AddNumbers an = new SumWS.AddNumbers();
//change the Url to the routed URL
an.Url = "http://notebook/WSEQuickStart/router/sumservice.asmx";
//create custom SoapHeader
an.p = new MyPathHeader();
//properties would be set on header object before calling
//its all done with default values in the constructor for this
int retVal = an.AddInt(1, 2);

Making those changes and I was finally able to call a WS-Routing web service from my Pocket PC! 1 down ... 2 to go 

UnkownHeaders

The SoapResponse returns with a Timestamp SoapHeader, but it is not set to mustUnderstand so that can be ignored. But if you dont want to ignore it, you can handle unknown headers on the client like this:

//declare member and attribute in auto gen'd proxy
public SoapUnknownHeader [] unknownHeaders;
[SoapHeaderAttribute("unknownHeaders",Direction=SoapHeaderDirection.Out)]

//and in client code with 'an' being the proxy
SoapUnknownHeader [] suha = an.unknownHeaders;
foreach(SoapUnknownHeader suh in suha)
{
	MessageBox.Show(suh.Element.Name);
}

This pops up a MessageBox showing wsu:Timestamp. The SoapUnknownHeader can be further reflected against for more meaningful info. On the full .NET Framework you can load the element into a Microsoft.Web.Services.Timestamp.Timestamp object t by calling t.LoadXml (suh.Element), although that does not work on CF .NET. More on Timestamps later

DIME

It is a message protocol that can contain SOAP messages. Usually it contains a SOAP message and 1 or more binary attachment(s). This saves a client from having to base64 en/decode attachments. Especially important on small client devices because base64 is a CPU cost and it makes for a larger payload with limited connection speeds. In other words, DIME is win/win for the PocketPC. An example being that MapPoint .NET should return map images as DIME attachments. Now, instead of messing with SoapHeaders to modify the SoapEnvelope internals, we have to modify the request and responses outside of the SoapEnvelope. The WSE implements DIME with a SoapExtension. The problem with this is that is uses classes like SoapServerMessage which do not exist on CF .NET. Lucky for us we can use the classes in the DIME namespace directly (the SoapExtension would normally use these classes indirectly for us). Of course I tried to use the classes from the WSE 1st ... they did not work. Was able to create objects, but certain calls would result in a MissingMethodException; possibly from the WSE assembly making a call to a method that does not exist in CF .NET. Now it gets real interesting

MSDN released an early DIME implementation called DimeSample of course. Guess what, it is implemented as a SoapExtension and all the classes are named the same with the same methods etc as the WSE. Cracking open Ildasm and Anakrino, they are very similar. So I opted to port a portion of this code to CF .NET in hopes that if a CF .NET client assembly is ever released then the client code could be written similarly.. The pertinent classes to change were: DimeWriter/Reader, DimeRecord, DimeAttachment, DimeStream,  and TypeFormatEnum. It built almost immediately, so I wrote the client code and pointed it to the WSE Attachments sample service. All it does is take a string DimeAttachment as input and then returns that string as a return value in the SoapResponse (non Dime). Since I was not using a web service proxy on the client, I used an HttpWebRequest, wrote the DIME stream directly, and then called for the HttpWebResponse. This made it quite clear that the implementations from DimeSample and WSE were not compatible ... probably spec changes. More Anakrino and Ildasm, I continued to modify the DimeSample to fit the WSE format (not production quality!). The TypeFormat and and Flags enumeration values had to be changed. Most of the changes were in the DimeRecord.WriteHeader(). Lots of bit | and << going on there. Tried alot of tools to help with this bit flipping process. The best ended up being DimeValidator, which the GotDotNet team released with source code. SoapToolkit3's Trace Utility has nice DIME support too, except if the message is not well formed; meaning it crashed early-on when my messages were way off. Did not try the SoapToolkit3 SP1

The client works by creating a DimeWriter with a stream. It then creates 2 DimeAttachments. The 1st DimeAttachment is the SoapRequest. The 2nd DimeAttachment is the input string to the Attachments WSE sample service. Both DimeAttachments are hydrated into DimeRecords. The DimeWriter stream is now prepped so I grab its contentLength. Set this on the HttpWebRequest and open the connection and get the request stream. Write the Dime stream out and then get the HttpWebResponse, which is a SoapEnvelope echoing the string that was sent as a DimeAttachment. This code also works on a full .NET framework app and the WSE lib with minimal changes. Just swap out the DimeWriter.CreateRecord() with the .NewRecord() and .LastRecord() messages (commented out)

string url = "http://192.168.1.102/wsequickstart/attachments/attachments.asmx";
HttpWebRequest hwReq = (HttpWebRequest) WebRequest.Create(url);
hwReq.Method = "POST";
hwReq.Timeout = 15000;
//hwReq.SendChunked = true;
hwReq.Headers.Add("SOAPAction", "\"http://microsoft.com/wse/samples/EchoAttachments/Echo\"");
hwReq.ContentType = "application/dime";
hwReq.KeepAlive = true;

MemoryStream ms = new MemoryStream();
DimeWriter dw = new DimeWriter(ms);
//dw.ChunkSize = 256;

string soapStr = "<?xml version \encoding \"1.0\""utf-8\"?><soap:Envelope xmlns:soap='\"http://schemas.xmlsoap.org/soap/envelope/\"' 
	xmlns:xsi='\"http://www.w3.org/2001/XMLSchema-instance\"' xmlns:xsd='\"http://www.w3.org/2001/XMLSchema\"'><soap:Header>
	</soap:Header><soap:Body><Echo xmlns='\"http://microsoft.com/wse/samples/EchoAttachments\"' /></soap:Body></soap:Envelope>";
byte [] soapBytes = System.Text.Encoding.UTF8.GetBytes(soapStr);
MemoryStream soapBuff = new MemoryStream(soapBytes);
DimeAttachment soapDa = new DimeAttachment("http://schemas.xmlsoap.org/soap/envelope/", TypeFormatEnum.AbsoluteUri, soapBuff);
int soapLen = soapDa.Stream.CanSeek ? (int)soapDa.Stream.Length : -1;
//DimeRecord soapDr = dw.NewRecord(soapDa.Id, soapDa.Type, soapDa.TypeFormat, soapLen);
Uri soapUri = new Uri(soapDa.Id);
DimeRecord soapDr = dw.CreateRecord(soapUri, soapDa.Type, soapDa.TypeFormat, soapLen);

BinaryCopy(soapDa.Stream, soapDr.BodyStream);
soapDa.Stream.Close();

byte [] bytes = System.Text.Encoding.UTF8.GetBytes("test_string");
MemoryStream buffer = new MemoryStream(bytes);
DimeAttachment da = new DimeAttachment("text/plain", TypeFormatEnum.MediaType, buffer);
int contentLength = da.Stream.CanSeek ? (int)da.Stream.Length : -1;
//DimeRecord dr = dw.LastRecord(da.Id, da.Type, da.TypeFormat, contentLength);
Uri daUri = new Uri(da.Id);
DimeRecord dr = dw.CreateRecord(daUri, da.Type, da.TypeFormat, contentLength);

BinaryCopy(da.Stream, dr.BodyStream);
da.Stream.Close();

hwReq.ContentLength = ms.Length;
Stream reqS = hwReq.GetRequestStream();

byte [] ba = ms.GetBuffer();
reqS.Write(ba, 0, (int) ms.Length);
reqS.Flush();
reqS.Close(); //unnecessary in the full framework

HttpWebResponse hwRes = (HttpWebResponse) hwReq.GetResponse();
Stream resS = hwRes.GetResponseStream();
StreamReader sr = new StreamReader(resS, System.Text.Encoding.ASCII);
string resXml = sr.ReadToEnd();
MessageBox.Show("SUCCESS " + resXml);

An Http sniff of the DimeRequest / SoapResponse. Here are the successful parsing results from DimeValidator

Validation Log:
####
RecordIndex = 0
Version = 1
MB = True
ME = False
TNF = 2
CF = False
ID Length = 41
Type Length = 41
Data Length = 889
Is Header Valid = True
Record Length = 992
Validation Log:
####
RecordIndex = 1
Version = 1
MB = False
ME = True
TNF = 1
CF = False
ID Length = 41
Type Length = 10
Data Length = 11
Is Header Valid = True
Record Length = 80
client message is complete

I only modified the DimeSample to send this message out (others will fail). The service can easily be modified to return DIME as well, but I did not modify the DimeReader from the DimeSample prototype to fit the WSE protocol changes. The bit flipping made it harder to understand than I appreciated. On the full Framework, the code to read a DIME response directly using the WSE assembly looks like this

DimeReader dRead = new DimeReader(resS);
DimeRecord dRec = dRead.ReadRecord();
if(dRec.TypeFormat != TypeFormatEnum.AbsoluteUri)
	throw new Exception("must be absoluteUri type");
if(dRec.Type != "http://schemas.xmlsoap.org/soap/envelope/")
	throw new Exception("must be soap type");
SoapEnvelope s = new SoapEnvelope();
s.Load(dRec.BodyStream);
dRec.Close();
DimeAttachmentCollection dac = new DimeAttachmentCollection(dRead);
foreach(DimeAttachment dAtt in dac)
{
	StreamReader sr = new StreamReader(dAtt.Stream, System.Text.Encoding.ASCII);
	MessageBox.Show(sr.ReadToEnd());
}

If somebody was getting paid to do this, the DimeSample could be made production quality (instead of my hack) to fully support DIME on CF .NET. 2 down ... 1 to go!

WS-Security

Did I say difficult :) Won't be much code here. WS-Security encompasses alot. From easiest to most difficult: Timestamp, UsernameSigning, SymmetricEncryption, X509Signing, and AsymmetricEncryption

Timestamp

We'll look at passing a Timestamp to the web service 1st, and then receiving one. Instead of the Sum and Router Web Services, this client will go against the Timestamp Web Service. All the service does is take the request (with or without a Timestamp) and then returns the response with a Timestamp that has a 60 second lifespan. The Timestamp header is real simple

<wsu:Timestamp xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
 <wsu:Created>2002-12-22T04:15:29Z</wsu:Created>
 <wsu:Expires>2002-12-22T04:20:29Z</wsu:Expires>
</wsu:Timestamp>

Microsoft.Web.Services.Timestamp.Timestamp does have a default constructor, but when I tried to set it as a SoapHeader it through the obligatory TypeLoadException. So I created my own again ... and it worked

//declare member and in/out header attribute in proxy too
//public MyTimestamp t;
//[SoapHeaderAttribute("t",Direction=SoapHeaderDirection.InOut)]

using System;
using System.Xml.Serialization;
using System.Web.Services.Protocols;

[XmlRoot(Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility", ElementName="Timestamp")]
public class MyTimestamp : SoapHeader
{
	public MyTimestamp()
	{
		Created = DateTime.Now;
		Expires = DateTime.Now.AddSeconds(10);
	}

	[XmlElement(Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility")]
	public DateTime Created;
	[XmlElement(Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility")]
	public DateTime Expires;
}

The client call will now look like this. If you uncomment the add -10 seconds the message will already be expired before it is sent so the service will throw a SoapHeaderException as expected. Also, the SoapResponse will repopulate the Timeout t object showing that the SoapResponse has 60 seconds to expire

TimestampWS.AddNumbers an = new TimestampWS.AddNumbers();
an.Timeout = 10000;

an.t = new MyTimestamp();
an.t.Created = DateTime.Now;
an.t.Expires = DateTime.Now.AddSeconds(10);
//an.t.Expires = DateTime.Now.AddSeconds(-10);

int retVal = an.AddInt(1, 2);
MessageBox.Show(retVal.ToString());

MessageBox.Show("created: " + an.t.Created.ToLongTimeString()
	+ "\r\nexpires: " + an.t.Expires.ToLongTimeString());

Looking back to the Router sample which returns a Timestamp. It has an extra element returned in the Timestamp header 


 <wsu:Received Actor="http://notebook/WSEQuickStart/router/sumservice.asmx" Delay="807">2002-12-22T04:15:29Z</wsu:Received>

Another class has to be created to support this and the MyTimestamp class must have it as a member

//add as member to MyTimestamp object as well
[XmlRoot(Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility", ElementName="Received")]
public class MyReceived
{
	public MyReceived(){}

	[XmlAttribute()]
	public string Actor;
	[XmlAttribute()]
	public int Delay;
	[XmlText()]
	public DateTime Text;
}

Ends up the Received element mod works on the full .NET Framework for return value Timestamps with the Receiver element, but throws an ArgumentNullException on CF .NET? The differences are the aggregation and XmlTextAttribute. I tried new'ing the member in the proxy since it was an Out header, still no luck. NOTE if you are using the Timestamp sample across different machines, you might need to synch up the time on those 2 machines or be more lenient with expirations. Also, take into consideration time zones and use UTC time

Digitally Signing

The WSE lets you digitally sign a message with a username/password, X.509 certificates, or a custom method. Further broken down, username/password signing the password can not be sent at all, sent as plaintext, or hashed. 1st off lets look at how verbose the WS-Security SoapHeader is when a username and hashed password is sent

<wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
<wsse:UsernameToken xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" wsu:Id="SecurityToken-6d081a42-a0f5-4931-a0fa-c684c20f2024">
    <wsse:Username>casey chesnut</wsse:Username>
    <wsse:Password Type="wsse:PasswordDigest">ePbjBngrdU6DllNo6BYaww15+7k=</wsse:Password>
    <wsse:Nonce>GgsvPKZbS7NS2a0THJzKXQ==</wsse:Nonce>
    <wsu:Created>2002-10-30T04:49:08Z</wsu:Created>
</wsse:UsernameToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
    <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
    <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1" />
    <Reference URI="#Id-48642eca-2e84-44c9-82f1-df75aaeabf5c">
        <Transforms>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>Loc2KCnIBRn2YtjnuHm7M2GIHvs=</DigestValue>
    </Reference>
    <Reference URI="#Id-696b74df-b5b7-4adc-be8b-9f87c31bfa21">
        <Transforms>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>E+BNLRy693uk0JC7N7DxB4P7ug8=</DigestValue>
    </Reference>
    <Reference URI="#Id-dc416a7f-67e6-4f6a-8957-ef34cecc3725">
        <Transforms>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>nu3jiEWxwrJ2clthGZBMnw6HtxE=</DigestValue>
    </Reference>
    <Reference URI="#Id-9cad0197-d15c-4990-a821-6a1d71d17a6c">
        <Transforms>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>/RNmQi82NqgX2KyE/ixFgEsSLTc=</DigestValue>
    </Reference>
    <Reference URI="#Id-4c85949b-88f3-4f0e-8222-5c6dd8488aa6">
        <Transforms>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>s59SfeQusWtX03ZwTWh3NR0qS8Q=</DigestValue>
    </Reference>
    <Reference URI="#Id-f54084c3-f42b-465b-8a07-29bd0a7e7320">
        <Transforms>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>Tx5uyCrRu6ZDqGpI6l7WKrwAxXk=</DigestValue>
    </Reference>
    </SignedInfo>
    <SignatureValue>1Jfj17mW0IBMYPD350Ksz9GGwnM=</SignatureValue>
    <KeyInfo>
    <wsse:SecurityTokenReference>
        <wsse:Reference URI="#SecurityToken-6d081a42-a0f5-4931-a0fa-c684c20f2024" />
    </wsse:SecurityTokenReference>
    </KeyInfo>
</Signature>
</wsse:Security>

If you think I am going to write that SoapHeader(s) ... you are crazy. Looking at it, most of it is boilerplate describing the hash used and id references, with most of the dynamic parts being in the UsernameToken and SignatureValue elements. I would use a SoapExtension in this case. The SoapExtension would have the Security header as boilerplate, fill in the dynamic parts, put it in the serialized SoapMessage and then it would be posted appropriately. The tricky part is getting the hashed value, which is in the next section about encryption. Also, I am not exactly sure how to pass the dynamic parameters to the SoapExtension? I am assuming there is some straightforward way; if not, there are definitely a couple hacked ways it can be done. To take this a little further i'll concentrate on the UsernameToken element and how to create those values using the following header as an example

<wsse:UsernameToken xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" wsu:Id="SecurityToken-b635d49f-dde2-4e2c-891c-677a965422ff">
	<wsse:Username>casey chesnut</wsse:Username>
	<wsse:Password Type="wsse:PasswordDigest">lX7Ixart5bHHww2iOiok2L5Cvgc=</wsse:Password>
	<wsse:Nonce>ycNWQ8/wK/NpBtSlmFDD1g==</wsse:Nonce>
	<wsu:Created>2002-12-28T05:10:22Z</wsu:Created>
</wsse:UsernameToken>

Username was obtained from Environment.UserName. For the WSE UsernameSigning sample the password is just the username reversed as a byte array and then base64 encoded. Nonce is a random byte array base64 encoded. Created is the serialized date. The password element is not really the password because of the Type=PasswordDigest attributes. So it is really a password digest which is a combination of nonce, creation, and the real password ... and then all of that is SHA1 hashed, which is then base64 encoded and represented as 'lX7Ixart5bHHww2iOiok2L5Cvgc='. From the WS-Security Addendum, this 'helps obscure the password and offers a basis for preventing replay attacks'. Using that knowledge, here is the code CF-compliant to re-create the PasswordDigest above

string strUsername = "casey chesnut";
//for username signing, the password is just the username reversed
byte[] passwordBytes = System.Text.Encoding.UTF8.GetBytes(strUsername);
Array.Reverse(passwordBytes, 0, passwordBytes.Length);
//then it is base 64 encoded
string passwordEquivalent = Convert.ToBase64String(passwordBytes,0,passwordBytes.Length);

string strNonce = "ycNWQ8/wK/NpBtSlmFDD1g==";
byte [] nonce = Convert.FromBase64String(strNonce);

string strCreated = "2002-12-28T05:10:22Z";	
DateTime dtCreated = XmlConvert.ToDateTime(strCreated);
string xmlStrCreated = XmlConvert.ToString(dtCreated, "yyyy-MM-ddTHH:mm:ssZ");

byte [] baCreated = Encoding.UTF8.GetBytes(strCreated);
byte [] baPassword = Encoding.UTF8.GetBytes(passwordEquivalent);
int baNonceLength = (int) nonce.Length + (int) baCreated.Length + (int) baPassword.Length;
byte [] baNonce = new byte[baNonceLength];
Array.Copy(nonce, 0, baNonce, 0, (int) nonce.Length);
Array.Copy(baCreated, 0, baNonce, (int) nonce.Length, (int) baCreated.Length);
Array.Copy(baPassword, 0, baNonce, (int) nonce.Length + (int) baCreated.Length, (int) baPassword.Length);

//SHA1 HASH CODE HERE
... returns buffer
//SEE ENCRYPTION SECTION BELOW

string passwordDigest = Convert.ToBase64String(buffer, 0, buffer.Length);
MessageBox.Show(passwordDigest);

Now for X.509 certificates. From my previous WSDK article, I still have a bitter taste from working with these (i.e. InvalidSecurityToken), even on the full Framework. Unluckily, the System.Security namespace for the CF .NET basically only includes functionality for X.509 certificates in System.Security.Cryptography.X509Certificates. All of the methods are there except for a couple of static ones and some constructor overloads. Granted, this might not be enough since the WSE includes the Microsoft.Web.Services.Security.X509 namespace. Regardless, the serialized SoapHeader for X509 looks similar to above with a BinarySecurityToken replacing the UsernameToken element. So my assumption here is that this would be possible as well

To prove that at least something could be done with X509 certificates on the PPC, wrote this little piece of code to just load a certificate and display its verbose .ToString() overload. The methods are also available to access the certificate keys as byte arrays. NOTE I tried this with certificates that had the .pfx extension, and they failed with an ArgumentException. Certificates exported with the .cer extension worked fine

string AppPath = System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase;
AppPath = AppPath.Substring(0, AppPath.LastIndexOf(@"\") + 1);
//string cerPath = AppPath + "OakLeaf Current User Cert.pfx"; //ArgumentException
string cerPath = AppPath + "exportedOakLeafCurrentUser.cer";
FileStream fs = new FileStream(cerPath,System.IO.FileMode.Open);
BinaryReader br = new BinaryReader(fs);
byte [] ba = br.ReadBytes((int)fs.Length);
X509Certificate x = new X509Certificate(ba);
MessageBox.Show(x.ToString(true)); //verbose overload

Encrypting

The WSE supports encrypting/decrypting outgoing/incoming SoapRequests/Responses. The client and server can share the same key for SymmetricEncryption, or it supports the public/private key model of AsymmetricEncryption. It is quite easy on the full framework; but as you might already know, the APIs for System.Security.Cryptography (except X.509 Certificates) is missing from the CF .NET. All of them, even the simple stuff like getting an MD5 or SHA1 hash. That doesn't mean Pocket PC has no support for cryptography, because it does (MSDN link). Pocket PC 2000 exposes encryption functionality through the rsabase.dll. Later, a high encryption pack became available in rsaenh.dll (the high encryption pack should not be installed on PPC 2002 because it is already there). Both of these DLLs expose the same methods, subset of the CryptoAPI, with rsaenh.dll supporting higher encryption lengths. This is the output of running dumpbin /exports on each of the DLLs

1    0 0000ABCC CPAcquireContext
2    1 00004E78 CPCreateHash
3    2 00004CC4 CPDecrypt
4    3 000066D0 CPDeriveKey
5    4 00005A90 CPDestroyHash
6    5 000076A8 CPDestroyKey
7    6 00005C00 CPDuplicateHash
8    7 000091C8 CPDuplicateKey
9    8 00004838 CPEncrypt
10    9 0000692C CPExportKey
11    A 000062BC CPGenKey
12    B 000092A8 CPGenRandom
13    C 00008B90 CPGetHashParam
14    D 00007C2C CPGetKeyParam
15    E 00008130 CPGetProvParam
16    F 000077D4 CPGetUserKey
17   10 000052DC CPHashData
18   11 0000577C CPHashSessionKey
19   12 00006DDC CPImportKey
20   13 0000AC30 CPReleaseContext
21   14 000086E0 CPSetHashParam
22   15 000078B0 CPSetKeyParam
23   16 00008078 CPSetProvParam
24   17 00009304 CPSignHash
25   18 000097B0 CPVerifySignature

With this it should be possible to PInvoke these methods from CF .NET. Well, it was possible with alot of help from Alex Feinman in the MS newsgroup. Here is how to get a SHA1 hash, since it is needed for Username signing above. You have to create PInvoke wrappers for the appropriate methods in coredll.dll. Then, the other 2 DLLs are called internally by it. For the client code, I used this sample. The declarations are listed, and then the client code. The corresponding managed code for the full Framework is also listed (commented out), and when I hashed the same string in both, the results were exactly the same. This is also the code that was used for doing the SHA1 hash for the UsernameSigning piece above

const int PROV_RSA_FULL = 1;
const int CALG_SHA1 = 32772;
const int HP_HASHVAL = 2;

[DllImport("coredll.dll")]
public static extern bool CryptAcquireContext(out IntPtr hProv, string pszContainer, string pszProvider, int dwProvType,int dwFlags);
[DllImport("coredll.dll")]
public static extern bool CryptCreateHash(IntPtr hProv, int Algid, IntPtr hKey, int dwFlags, out IntPtr phHash);
[DllImport("coredll.dll")]
public static extern bool CryptHashData(IntPtr hHash, byte [] pbData, int dwDataLen, int dwFlags);
[DllImport("coredll.dll")]
public static extern bool CryptGetHashParam(IntPtr hHash, int dwParam, byte[] pbData, ref int pdwDataLen, int dwFlags);
[DllImport("coredll.dll")]
public static extern bool CryptDestroyHash(IntPtr hHash);
[DllImport("coredll.dll")]
public static extern bool CryptReleaseContext(IntPtr hProv, int dwFlags);

//SHA1 sha1 = new SHA1CryptoServiceProvider();
//byte [] ba = System.Text.Encoding.ASCII.GetBytes("password");
//byte [] hash = sha1.ComputeHash(ba);
IntPtr hProv;
bool retVal = CryptAcquireContext( out hProv, null, null, PROV_RSA_FULL, 0 );
IntPtr hHash;
retVal = CryptCreateHash( hProv, CALG_SHA1, IntPtr.Zero, 0, out hHash );
string pass = "password";
byte [] publicKey = System.Text.Encoding.ASCII.GetBytes(pass);
int publicKeyLen = publicKey.Length;
retVal = CryptHashData( hHash, publicKey, publicKeyLen, 0 );
int bufferLen = 20; //SHA1 size
byte [] buffer = new byte[bufferLen];
retVal = CryptGetHashParam( hHash, HP_HASHVAL, buffer, ref bufferLen, 0 );
retVal = CryptDestroyHash( hHash );
retVal = CryptReleaseContext( hProv, 0 );
string hash = System.Text.Encoding.ASCII.GetString(buffer, 0, bufferLen);
MessageBox.Show("HASH " + hash);

MSN Messenger

When looking around for encryption code, I ran across a C# MD5 implementation. This reminded me that I had been trying to send an MSN Messenger instant message in one of my previous CF .NET apps without having to use Pocket Messenger. Had ported the code from another MSN Messenger library for the full .NET Framework to CF .NET. The one piece missing was an MD5 hash. Just for kicks I threw that together. With the library below it lets you send MSN messages directly from your CF .NET apps. The client code looks like this NOTE the SHA1 hash code above could me modified to do MD5 instead

//add MsnImPpcLib namespace
string user = "from@hotmail.com";
string password = "PASSWORD";
string who = "to@hotmail.com.com";
string what = "message";
ImClient im = new ImClient(user, password);
ImSession session = im.RequestSession(who);
session.Send(what);

Conclusion

The 1st gen web service stack works great, but is not quite adequate for enterprise level applications. The 2nd gen web service stack adds the necessary functionality to support real-world web services, and now has released and supported implementations like the WSE. On small devices, the RTM version of CF .NET now has great support for the 1st gen web service stack. With some extra work, CF .NET can also be made to consume 2nd-gen WSE web services to build mobile enterprise applications. In this article we successfully used WS-Routing and DIME, along with some initial headway into WS-Security

In the future I hope the CF .NET and WSE teams will provide a client-side assembly to directly support the client-side programming model of the WSE on small devices. This would probably entail a managed wrapper of the high encryption APIs or a managed assembly to provide System.Security functionality. Finally, I'd like to see something like FxCop for CF .NET (ala CFxCop). So that when people are developing reusable assemblies they can easily see what will and wont work with the SDE

Source

Future

Might revisit this. Also might do another WSE article with a server side concentration looking at the more advanced features added since the WSDK. Definitely once they do another release to support some of the new specs that are coming out ... 6 new ones were just released mid December 2002. Can't wait for the SmartPhone support to make it into SDE as well

HELP! Please let me know if there are any new betas that are available (wish betaplace had a listing of running / approaching betas OR would just automatically sign me up). Honestly, I have not had many cool ideas lately :( Will start downing mass quantities of ginkgo biloba if this dry spell continues :) Of course I did notice DirectX 9! Later