Monday, May 22, 2006
« Resharper 2.0 Released Today | Main | Introduction to C# Presentation »

I'm developing a web service in ASP.NET 1.1 through which I need to transfer files.  There are a myriad ways in which I could accomplish this, but rather than roll my own, which I am sometimes inclined to do, I wanted to use some built-in functionality.  This functionality can be found in WSE 2.0 and DIME (Direct Internet Message Encapsulation) - thanks Fabio for reminding me.  Believe me, it pains me that I cannot use WSE 3.0 and MTOM with .NET 2.0, but such is not possible with this project - at least not yet.  But transferring the files would be orders of magnitude easier via MTOM, but alas, here we are.

Shortly after I set up the web service and tested it locally, I wanted to ensure that it would work remotely.  The machine to which I have it deployed is running on a Virtual PC running Windows Server 2003.  I could see the nice ASP.NET web service description page navigating via my browser to the .ASMX file, but when I attempted to access it via code, I was first presented with the following error message:

System.Net.WebException: The request failed with HTTP status 401: Unauthorized.
   at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
   at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
...

Oh, that's right, I need to include some credentials to access this particular web service.  Therefore, I added the following snippet of code below my client proxy declaration:

ServiceWse svc = new ServiceWse();
svc.Credentials = CredentialCache.DefaultCredentials;

Once I did that and ran my test harness I was greeted with a slightly more inocuous error:

Microsoft.Web.Services2.Security.SecurityFault: An error was discovered processing the <Security> header ---> System.Exception: Creation time in the timestamp can not be in the future.
--- End of inner exception stack trace ---
at Microsoft.Web.Services2.Security.Utility.Timestamp.CheckValid()
at Microsoft.Web.Services2.Security.Utility.Timestamp.LoadXml(XmlElement element)
at Microsoft.Web.Services2.Security.Utility.Timestamp..ctor(XmlElement element)
at Microsoft.Web.Services2.Security.Security.LoadXml(XmlElement element)
at Microsoft.Web.Services2.Security.SecurityInputFilter.ProcessMessage(SoapEnvelope envelope)
at Microsoft.Web.Services2.Pipeline.ProcessInputMessage(SoapEnvelope envelope)
at Microsoft.Web.Services2.InputStream.GetRawContent()
at Microsoft.Web.Services2.InputStream.get_Length()
at System.Xml.XmlScanner..ctor(TextReader reader, XmlNameTable ntable)
at System.Xml.XmlTextReader..ctor(String url, TextReader input, XmlNameTable nt)
at System.Xml.XmlTextReader..ctor(TextReader input)
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
...

As it turns out, WSE will utilize a timestamp to verify security tokens between the SOAP sender and receiver and if the two computers' clocks vary by more that the default tolerance of five minutes, it will fail.

One solution would be to synchronize the client to the server, but that's a pain and may not properly work in a global scale which would be required by my particular application.

This baffled me for a few minutes because the time on my local PC and the time on my VPC were the same; that is, they appeared the same.  I quickly saw that the VPC's time, though the same with respect to hrs, minutes, and seconds is relative to Pacific Time whereas my laptop is on Mountain Time.  So they ultimately varied by 60 minutes, well over the default tolerance.

The solution is to alter the web.config file on the server and the client application's configuration file to include an override to the tolerance.  I set it to allow for up to a day (24 hrs) in difference to accommodate for any discrepancy that may arise.

<configuration>
   <configSections>
      <section name="microsoft.web.services2" type="Microsoft.Web.Services2.Configuration.WebServicesConfiguration, Microsoft.Web.Services2, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
   </configSections>
   <microsoft.web.services2>
      <security>
         <timeToleranceInSeconds>86400</timeToleranceInSeconds>
      </security>
   </microsoft.web.services2>
</configuration>

I'm actually very happy that I had this situation arise now rather than after I had deployed because I now remember that I have to test for time zone differences.