I’m currently finishing on a project which uses WSE 2.0 SP3 as a communication platform. At one point I’ve had an interesting problem, which I couldn’t find solution for (at least not according to the specs and WSE documentation). So I’ve decided to share my knowledge on the matter and how I’ve solved it, in case anyone else finds it difficult to deal with a similar situation.
So what’s the problem?
I was implementing a SOAP over SMTP messaging infrastructure for sending XML messages through email, because of the fact that the backend of the application can’t be published on the internet for security reasons. So we’ve done a proof of concept (Paul did that) which was working like a charm.
Then when I took over and started to put the whole thing together, I’ve got a strange problem with a web service published in WSS site, which was called through the Soap over Smtp “component”. The problem was, that Sharepoint needs a windows identity that should be verified by IIS (sharepoint uses IIS for the authentication process). So even if you do a local LogonUser, WSS/SPS won’t be happy because of some strange reason that I couldn’t find. So now I had to make the call on the behalf of a user that has to be authenticated against IIS (so Active Directory or a local Windows account) and then, eventually I was going to be able to do my thing in Sharepoint. That means that along with the message relay though my SoapReciver, I had to send a windows identity. It turned out that this is not an easy thing to do with WSE 2.0 (SP3).
What did I try?
1. Obtaining the context of the SoapHttpOutputChannel and setting the PreAuthenticate property to true, along with the Credentials to System.Net.CredentialCache.DefaultCredentials (my service that relays the messages is logged on the same machine, so I should have been able to use the credentials).
The “obtaining” part was done in a custom SoapOutputFilter that was called for every message that was send.
That doesn’t work.
It fails with a strange exception, that has been partially documented with ASP.NET 1.1, but doesn’t work with ASP.NET 2.0 as well. How to test it and see that it doesn’t work? Here’s a simple program that’s sending a single message to a very, very simple web service:
try
{
SoapEnvelope message = new SoapEnvelope();
message.Context.Addressing.Action = new Action("http://tempuri.org/HelloWorld");
Uri urlTo = new Uri("http://localhost:7357/WSTest/Service.asmx");
SoapSender sender = new SoapSender(urlTo);
message.Body.InnerXml = "<HelloWorld xmlns=\"http://tempuri.org/\" />";
sender.Send(message);
}
catch ( Exception ex )
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
And here is the filter that’s been called:
public class Output : SoapOutputFilter
{
public override void ProcessMessage(SoapEnvelope envelope)
{
if ( null!= envelope.Context )
{
ISoapChannel channel = envelope.Context.Channel;
SoapHttpOutputChannel httpChannel = channel as SoapHttpOutputChannel;
if ( null!=httpChannel )
{
httpChannel.Options.PreAuthenticate = true;
httpChannel.Options.Credentials = CredentialCache.DefaultCredentials;
}
}
}
}
And finally, here’s the exception:
The operation has timed-out.
at System.Net.HttpWebRequest.GetRequestStream()
at Microsoft.Web.Services2.Messaging.SoapHttpTransport.Send(SoapEnvelope mess
age, EndpointReference destination, SoapHttpChannelOptions options)
at Microsoft.Web.Services2.Messaging.SoapHttpOutputChannel.Send(SoapEnvelope message)
at Microsoft.Web.Services2.Messaging.SoapSender.Send(SoapEnvelope envelope) at ConsoleTest.TheMainClass.Main(String[] args) in c:\documents and settings\branimir\my documents\visual studio projects\consoletest\main.cs:line 29
If you want to test it yourself, download the code and run it. The web service is a ASP.NET 2.0 WS, so you need that installed. Along with WSE 2.0 SP3 for the console app.
2. Obtaining the SoapHttpOutputChannel before sending the message. Doesn’t work as well. The problem here is that sending the message has nothing to do with what protocol will be used for that. You should be able to add or remove protocols though the configuration of the app, so this way you can replace the default Http sender with a custom HttpSoapTransport. That really makes sence. Actually this is how I finally cracked it :) – see #3 for details on this one.
I’ve tried the following to get a reference to the SoapHttpOutputChannel instance:
EndpointReference endpoint = new EndpointReference(urlTo);
ISoapTransport transport = SoapHttpTransport.GetTransport(endpoint);
SoapHttpTransport httpTransport = transport as SoapHttpTransport;
if ( null!=httpTransport )
{
SoapHttpOutputChannel httpOutputChannel =
httpTransport.GetOutputChannel(endpoint, SoapChannelCapabilities.None) as SoapHttpOutputChannel;
httpOutputChannel.Options.Credentials = CredentialCache.DefaultCredentials;
httpOutputChannel.Options.PreAuthenticate = true;
}
3. Replacing the default SoapHttpTransport with a custom implementation that does the authentication by default. It’s not so easy to write that (and test it so you can be sure that it works).
So I’ve decided to decompile what Microsoft did, and customize it.
Then I’ve added a new protocol scheme for using integrated authentication called “http.auth://” and used it when I wanted to make calls that authenticate with Windows Integrated.
Check out the attachments for this post for the complete source code.
Initially I wanted to just change the default options of the default SoapHttpTransoport, but that turned out the be impossible. I kept getting the same exception as in #1.
So I’m guessing that there is a problem with the Options of the protocol (don’t think that they’ve tested it good enough – the options are not getting initialized anyway).
At that point I’ve already decompiled MS’s implementation and I’ve filled in the gaps, so it was actually working. I’ve added a couple of lines of code to the method that does the actual call and it was ready. It actually worked
. Wohooo - I have a SoapHttpTransport with a Windows Authentication of my own, and it's working
.
If you wan to use this one, once you complile the transport, you need to add the following lines to your config file (the wse2 section should be there already):
<microsoft.web.services2>
<messaging>
<transports>
<add scheme="http.auth" type="SofiaDev.Wse.SoapHttpTransport, SofiaDev.Wse" />
</transports>
</messaging>
</microsoft.web.services2>
Then, use http:auth:// as a transport scheme instead of http. This way WSE knows which transport protocol implementation to instantiate, based on what it finds at the config file.