Adapting to the caller namespace soap 1.1 request in classic asmx via Filter

 

I had received a somewhat odd request to produce a proof-of-concept application to enable a classic asmx Web Services to accept any namespace in the soap 1.1 request and to return the same namespace to the caller. To understand the problem let us review the soap 1.1 requirement on what it matters. The Soap Action comes as an Http header SoapAction containing the namespace and the operation. In the body the operation is one of the elements in the soap body with the namespace of the service.

This is the sample web services used to test the proof of concept:

[WebService(Namespace = "https://MyNameSpace.com")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]              public class NoNameSpace : System.Web.Services.WebService {

    public NoNameSpace () {      }

    [WebMethod]     public string HelloWorld(string Name) {         return String.Format("Hello {0}", Name);     }

Table 1 – Code for the service using namespace https://MyNameSpace.com

That is a correct soap 1.1 request. Notice that the soap action is the namespace and operation and the HelloWorld element uses the namespace:

01:POST /NoNameSpace.asmx HTTP/1.1           02: Host: localhost 03: Content-Type: text/xml; 04: charset=utf-8 05: Content-Length: length 06: SOAPAction: "https://MyNameSpace.com/HelloWorld" 07: 08: <?xml version="1.0" encoding="utf-8"?> 09: <soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/ "> 10: <soap:Body> 11: <HelloWorld xmlns="https://MyNameSpace.com"> 12: <Name>string</Name> 13: </HelloWorld> 14: </soap:Body> 15: </soap:Envelope>

Table 2 – Typical raw soap request with headers and body

The response will include the HellowWorldResponse element with the namespace used by the service.

01: HTTP/1.1 200 OK 02:Content-Type: text/xml; 03: charset=utf-8 04: Content-Length: length 05: 06: <?xml version="1.0" encoding="utf-8"?> 07: 08: <soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/ "> 09: <soap:Body> 10: <HelloWorldResponse xmlns="https://MyNameSpace.com"> 11: <HelloWorldResult>string</HelloWorldResult> 11: </HelloWorldResponse> 12: </soap:Body> 13: </soap:Envelope>

Table 3 – Raw soap response from request in Table 2

In customer’s scenario they had some clients consuming their web services using some soap tool he could not identify that always sent the target URL as the namespace. Though they could change the namespace to always match their final production environment URL, they do not want to have to change the namespace if they decide to use another URL in production.

I strongly advised them to use the target namespace solution as it will the solution causing less overhead. They requirements were very clear though. I asked them some time to develop a proof-of-concept filter to:

  1. Intercept the original call and verify the SoapAction header to verify if the namespace matches the expected web services namespace (see Table 2 row 6)
  2. If the namespace matches, nothing else to do, just process the request without the overhead of filtering
  3. If the namespace does not match, separate the incoming namespace and operation, save values for later use
  4. Replace the SoapAction header to include the web services expected namespace and operation (see Table 2 row 6)
  5. Add a request stream filter to replace the namespace of the operation element to match the expected namespace (see Table 2 row 11)
  6. Add a response stream filter to replace the response namespace to match the original incoming namespace as it is what the consumer client is expecting
  7. The response element is by default the soap action + Response (see Table 3 row 10)

 

For example, this is a request in customer’s environment:

01:POST /NoNameSpace.asmx HTTP/1.1           02: Host: localhost 03: Content-Type: text/xml; 04: charset=utf-8 05: Content-Length: length 06: SOAPAction: "https://10.10.8.5:9000/HelloWorld" 07: 08: <?xml version="1.0" encoding="utf-8"?> 09: <soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/ "> 10: <soap:Body> 11: <HelloWorld xmlns="https://10.10.8.5:9000"> 12: <Name>string</Name> 13: </HelloWorld> 14: </soap:Body> 15: </soap:Envelope>

Table 4 – Request with invalid namespace in both SoapAction header and element HelloWorld in the soap body

Please notice that both soap action (row 6) and namespace (row 11) come as the URL used to access the service, not the real namespace specified in the service wsdl. The input filter will bring the request in Table 4 to the same state as request in Table 2 before reaching the web service.

The response from the web service will be as Table 3 including the actual namespace not the namespace sent in the request. The output filter will transform the correct namespace into the incoming namespace as shown below:

01: HTTP/1.1 200 OK 02:Content-Type: text/xml; 03: charset=utf-8 04: Content-Length: length 05: 06: <?xml version="1.0" encoding="utf-8"?> 07: 08: <soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/ "> 09: <soap:Body> 10: <HelloWorldResponse xmlns="https://10.10.8.5:9000"> 11: <HelloWorldResult>string</HelloWorldResult> 11: </HelloWorldResponse> 12: </soap:Body> 13: </soap:Envelope>

Table 5 – Raw soap response after adjusting namespace to match incoming namespace request (see row 10)

The best way to implement the logic, as it is classic asmx is in the Global.asax. I have just added a Global.asax to the project and changed the code inside to the listing below:

<%@ Application Language="C#" %> <%@ Import Namespace="System.Text" %> <%@ Import Namespace="System.IO" %> <%@ Import Namespace="System.Xml" %>

<script runat="server">

    //The code samples are provided AS IS without warranty of any kind. // Microsoft disclaims all implied warranties including, without limitation, // any implied warranties of merchantability or of fitness for a particular purpose.

    /*

    The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.

    */ 

    // Change this string to the string used in your web services namespace. Do not include "/" at the end.     const string NAMESPACE = "https://MyNameSpace.com";     void Application_Start(object sender, EventArgs e)     {         // Code that runs on application startup

    }     void Application_End(object sender, EventArgs e)     {         // Code that runs on application shutdown

    }

    void Application_BeginRequest(object sender, EventArgs e)     {         HttpContext ctx = HttpContext.Current;         // Soap Action comes in the Http Header SoapAtion // Ex.: SoapAction: "https://MyNameSpace.com/HelloWorld"         string tempAction = ctx.Request.Headers["SoapAction"];         // Soap action is normally surrounded by quotes so we are removing for this operation temporary string. We are not changing SoapAction yet         string soapAction = String .IsNullOrEmpty(tempAction) ? null : tempAction.Replace("\"", "");

        // If there is no Soap Action in the call and the namespace is coming as expected, do not bother to filter anything         if (!String .IsNullOrEmpty(soapAction) && !soapAction.StartsWith(NAMESPACE))         {             // In the end "ns" will contain the namespace as coming from the caller // "operation" will contain only the operation without the namespace // For example: SoapAction: "https://roguenamespace.local/HelloWorld" // will be split so that: ns=https://roguenamespace.local and operation=HelloWord // If the namespace is not matching the expected namespace and the operation is not matching the right operation // the webservices will not work. // For the scenario above, the expected Class and Method definition are: // // [WebService(Namespace = "https://roguenamespace.local")] // [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] // public class NoNameSpace : System.Web.Services.WebService // (...) // [WebMethod] // public string HelloWorld(string Name) //             string ns=NAMESPACE;             string operation=soapAction;

            int i=soapAction.LastIndexOf('/');             if(i>=0 && i< soapAction.Length)             {                 try                 {                                    operation = soapAction.Substring(i+1);                     ns = soapAction.Substring(0,i);                     //                     // We verified that the incoming namespace is not matching the expected namespace before, // now we are going to adapt the SoapAction header to include the expected namespace and the intended operation //                     ctx.Request.Headers["SoapAction"] = String.Format("\"{0}/{1}\"", NAMESPACE, operation);                 }                 catch (Exception ex)                 {                     ctx.Request.Headers["SoapAction"] = String.Format("{0}<br />{1}", ex.Message, ex.StackTrace);                 }             }             else             // Since we are sure by the inner if that SoapAction is not empty or null you can assign directly. // This code only runs if there is a SoapAction without namespace - SoapAction: "HelloWorld"             {                 operation = ctx.Request.Headers["SoapAction"] = String.Format("\"{0}/{1}\"", NAMESPACE, operation);             }             // // Add original soap action to change the response as well // This item will be included in the Http Context items and can be tested in your code later // if you need to make decisions based on any of them //             ctx.Items.Add(NAMESPACE, new string[]{ operation, ns });

            try             {                 // Now we will create a filter that will change the incoming namespace for the operation with the intended namespace (in NAMESPACE const) // in the message body // For example: // This request contains the invalid namespace https://roguenamespace.local in soap body element HelloWorld // This filter will change the xmlns attribute to match the expected namespace to the one in NAMESPACE const                 // //<?xml version="1.0" encoding="utf-8"?> //<soap:Envelope (...)"> // <soap:Body> // <HelloWorld xmlns="https://roguenamespace.local"> ===> Will become ==> <HelloWorld xmlns="https://MyNameSpace.com"> // <Name>string</Name> // </HelloWorld> // </soap:Body> //</soap:Envelope> //                 FilterNameSpace stream = new FilterNameSpace(ctx.Request.Filter);                 stream.SetChange(soapAction, NAMESPACE, operation, false);                 ctx.Request.Filter = stream;                 // We are also making sure the response will return the same rogue namespace send by the caller // See details in the method SetResponseFilter                 SetResponseFilter();

            }             catch             {                 ctx.Request.InputStream.Position = 0; // Reset position if error happens                 return;             }             if (String.IsNullOrEmpty(ns))             {

            }             else             {             }         }

    }

    void SetResponseFilter()     {         // We will add a filter to change the correct namespace in the response to the caller rogue namespace // The response element, by default, is the operation name + Response. // In this sample the element is HelloWorldResponse // // The change will look like that (only the changed line): // // <HelloWorldResponse xmlns="https://MyNameSpace.com"> ===> Will become ===> <HelloWorldResponse xmlns="https://roguenamespace.local">         HttpContext ctx = HttpContext.Current;         if (ctx.Items.Contains(NAMESPACE))         {             string[] reqinfo = ctx.Items[NAMESPACE] as string[];             if (reqinfo == null || reqinfo.Length < 2) return;             string opResp = String.Format("{0}Response", reqinfo[0]);             string ns = reqinfo[1];             FilterNameSpace outfilter = new FilterNameSpace(ctx.Response.Filter);             outfilter.SetChange(NAMESPACE, ns, opResp, true);

            ctx.Response.Filter = outfilter;                        }

    }

    void Application_PostRequestHandlerExecute(object sender, EventArgs e)     {         // Code that runs when an unhandled error occurs

    }

    void Session_Start(object sender, EventArgs e)     {         // Code that runs when a new session is started

    }

    void Session_End(object sender, EventArgs e)     {         // Code that runs when a session ends. // Note: The Session_End event is raised only when the sessionstate mode // is set to InProc in the Web.config file. If session mode is set to StateServer // or SQLServer, the event is not raised.

    }

    class FilterNameSpace : Stream     {

        protected Stream inputStream;         protected Stream sinkStream;         protected bool changed;         protected string newNameSpace;         protected string originalNS;         protected string operation;         protected StringBuilder strOutput;         protected bool canWrite;         public FilterNameSpace(Stream Sink)         {             sinkStream = Sink;             changed = false;         }

        public void SetChange(string OriginalNameSpace, string NewNameSpace, string Operation, bool CanWrite)         {             originalNS = OriginalNameSpace;             newNameSpace = NewNameSpace;             operation = Operation;             inputStream = new MemoryStream();             canWrite = CanWrite;

        }         public void DocumentStream()         {             if(changed) return;             XmlDocument doc = new XmlDocument();             inputStream.Position = 0;             if (canWrite)                 doc.Load(inputStream);             else                 doc.Load(sinkStream);             System.Xml.XmlNodeList nodes = doc.GetElementsByTagName(operation);             if (nodes.Count > 0)             {                 string prefix = nodes[0].GetPrefixOfNamespace(newNameSpace);                 nodes[0].Attributes["xmlns"].Value = newNameSpace;

            }             changed = true;             inputStream.Position = 0;             if (canWrite)                 doc.Save(sinkStream);             else                 doc.Save(inputStream);             inputStream.Position = 0;         }         public override bool CanRead         {             get { return true; }         }

        public override bool CanSeek         {             get { return false; }         }

        public override bool CanWrite         {             get { return true; }         }

        public override long Length         {             get {                 DocumentStream();                 return inputStream.Length;             }         }

        public override long Position         {             get { return inputStream.Position; }             set { throw new NotSupportedException(); }         }

        public override int Read(byte[] buffer, int offset, int count)         {             DocumentStream();             int c = (int)((long)count > inputStream.Length ? inputStream.Length : (long)count);             c=inputStream.Read(buffer, offset, count);                          return c;         }

        public override long Seek(long offset, System.IO.SeekOrigin direction)         {             throw new NotSupportedException();         }

        public override void SetLength(long length)         {             throw new NotSupportedException();         }

        public override void Close()         {             inputStream.Close();             sinkStream.Close();         }

        public override void Flush()         {             DocumentStream();             inputStream.Flush();             sinkStream.Flush();         }

        public override void Write(byte[] buffer, int offset, int count)         {             inputStream.Write(buffer, offset, count);         }

    } </script>

Table 6 – Code to accomplish the namespace substitution