Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to send data as a SoapMessage and get a reply?

Tags:

c#

soap

I have some data that needs to be send in SOAP format to a server. This server will immediately acknowledge that it received the messages. After a few hours I get (possibly from another server) a SOAP message that contains information about the processed data.

I read Stackoverflow: How to send SOAP request and receive response. However, the answers are 8 years old. Although they may still work, It may be that there are newer techniques.

And indeed it seems: Microsoft has System.Web.Services.Protocols, with classes like SoapMessage, SoapClientMessage, SoapServerMessage, etc.

Looking at the classes I find a lot of SOAP like classes (headers, extensions, client messages, server messages... Normally the provided examples give me an indication to how these classes work together and how to use them. In the MSDN documents I can only find examples of how to process already existing SOAP messages.

Given some data that needs to be sent, how can I wrap this data somehow in one of these SOAP classes and send this message?

Are these classes meant for this purpose? Or should I stick to the 2011 method where you'd create a SOAP Web request by formatting the XML data in soap format yourself, as the above mentioned Stackoverflow question suggests?

I'm awfully sorry, normally I would write things I have tried. Alas I don't see the relation between the provided SoapMessage classes. I haven't got a clue how to use them.

Addition after comments

I'm using windows server / visual studio (newest versions) / .NET (newest versions) / C# (newest versions).

The communication with the server is mutual authenticated. The certificate that I need to use to communicate with the server, is in PEM (CER / CRT) format. The privated key is RSA. This certificate is issued by a proper CA, the server will also use certificates used by a proper CA. So I don't need to create a new certificate (in fact, it won't be accepted). If needed, I'm willing to convert the certificates using programs like OpenSsl and the like.

I've tried to use Apache TomCat to communicate, but I have the feeling that that's way too much for the task of sending one SOAP message per day and waiting for one answer per day.

Maybe because java is a complete new technique for me, it was difficult for me to see the contents of the received messages. So back to C# and .NET.

I was planning to create a DLL, to be used by a console app. The function would have some data in a stream as input. It would create the soap message, send it, wait for reply that the message was received correctly, and wait (possible several hours) for a new Soap message containing the results of the processed data. To make proper reporting, and cancellation possible, I guess it is best to do this using async-await

If sending the order and waiting for the result can't be done in one application, I'm willing to create a windows service that that listens to the input, but I prefer to keep it simple.

The (virtual) computer will only be used for this task, so no one else will need to listen to port 443. There will be one order message send per day, and one result message per day.

like image 978
Harald Coppoolse Avatar asked Apr 08 '19 07:04

Harald Coppoolse


2 Answers

Here is sample C# Console client and server code (they are in the same sample but this is only for demo purpose, of course) that uses HTTPS.

For the client side, we reuse the SoapHttpClientProtocol class, but for the server side, unfortunately, we cannot reuse anything because classes are completely tied to ASP.NET's (IIS) HttpContext class

For the server side, we use HttpListener, so, depending on your configuration, the server side will probably require admin rights to be able to call HttpListener's Prefixes.Add(url).

The code doesn't uses client certificate, but you can add this where I placed // TODO comments

The code assumes there is a certificate associated with the url and port used. If there's not (use netsh http show sslcert to dump all associated certs), you can use the procedure described here to add one: https://stackoverflow.com/a/11457719/403671

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;

namespace SoapTests
{
    class Program
    {
        static void Main(string[] args)
        {
            // code presumes there is an sslcert associated with the url/port below
            var url = "https://127.0.0.1:443/";
            using (var server = new MyServer(url, MyClient.NamespaceUri))
            {
                server.Start(); // requests will occur on other threads
                using (var client = new MyClient())
                {
                    client.Url = url;
                    Console.WriteLine(client.SendTextAsync("hello world").Result);
                }
            }
        }
    }

    [WebServiceBinding(Namespace = NamespaceUri)]
    public class MyClient : SoapHttpClientProtocol
    {
        public const string NamespaceUri = "http://myclient.org/";

        public async Task<string> SendTextAsync(string text)
        {
            // TODO: add client certificates using this.ClientCertificates property
            var result = await InvokeAsync(nameof(SendText), new object[] { text }).ConfigureAwait(false);
            return result?[0]?.ToString();
        }

        // using this method is not recommended, as async is preferred
        // but we need it with this attribute to make underlying implementation happy
        [SoapDocumentMethod]
        public string SendText(string text) => SendTextAsync(text).Result;

        // this is the new Task-based async model (TAP) wrapping the old Async programming model (APM)
        public Task<object[]> InvokeAsync(string methodName, object[] input, object state = null)
        {
            if (methodName == null)
                throw new ArgumentNullException(nameof(methodName));

            return Task<object[]>.Factory.FromAsync(
                beginMethod: (i, c, o) => BeginInvoke(methodName, i, c, o),
                endMethod: EndInvoke,
                arg1: input,
                state: state);
        }
    }

    // server implementation
    public class MyServer : TinySoapServer
    {
        public MyServer(string url, string namespaceUri)
            : base(url)
        {
            if (namespaceUri == null)
                throw new ArgumentNullException(nameof(namespaceUri));

            NamespaceUri = namespaceUri;
        }

        // must be same as client namespace in attribute
        public override string NamespaceUri { get; }

        protected override bool HandleSoapMethod(XmlDocument outputDocument, XmlElement requestMethodElement, XmlElement responseMethodElement)
        {
            switch (requestMethodElement.LocalName)
            {
                case "SendText":
                    // get the input
                    var text = requestMethodElement["text", NamespaceUri]?.InnerText;
                    text += " from server";

                    AddSoapResult(outputDocument, requestMethodElement, responseMethodElement, text);
                    return true;
            }
            return false;
        }
    }

    // simple generic SOAP server
    public abstract class TinySoapServer : IDisposable
    {
        private readonly HttpListener _listener;

        protected TinySoapServer(string url)
        {
            if (url == null)
                throw new ArgumentNullException(nameof(url));

            _listener = new HttpListener();
            _listener.Prefixes.Add(url); // this requires some rights if not used on localhost
        }

        public abstract string NamespaceUri { get; }
        protected abstract bool HandleSoapMethod(XmlDocument outputDocument, XmlElement requestMethodElement, XmlElement responseMethodElement);

        public async void Start()
        {
            _listener.Start();
            do
            {
                var ctx = await _listener.GetContextAsync().ConfigureAwait(false);
                ProcessRequest(ctx);
            }
            while (true);
        }

        protected virtual void ProcessRequest(HttpListenerContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            // TODO: add a call to context.Request.GetClientCertificate() to validate client cert
            using (var stream = context.Response.OutputStream)
            {
                ProcessSoapRequest(context, stream);
            }
        }

        protected virtual void AddSoapResult(XmlDocument outputDocument, XmlElement requestMethodElement, XmlElement responseMethodElement, string innerText)
        {
            if (outputDocument == null)
                throw new ArgumentNullException(nameof(outputDocument));

            if (requestMethodElement == null)
                throw new ArgumentNullException(nameof(requestMethodElement));

            if (responseMethodElement == null)
                throw new ArgumentNullException(nameof(responseMethodElement));

            var result = outputDocument.CreateElement(requestMethodElement.LocalName + "Result", NamespaceUri);
            responseMethodElement.AppendChild(result);
            result.InnerText = innerText ?? string.Empty;
        }

        protected virtual void ProcessSoapRequest(HttpListenerContext context, Stream outputStream)
        {
            // parse input
            var input = new XmlDocument();
            input.Load(context.Request.InputStream);

            var ns = new XmlNamespaceManager(new NameTable());
            const string soapNsUri = "http://schemas.xmlsoap.org/soap/envelope/";
            ns.AddNamespace("soap", soapNsUri);
            ns.AddNamespace("x", NamespaceUri);

            // prepare output
            var output = new XmlDocument();
            output.LoadXml("<Envelope xmlns='" + soapNsUri + "'><Body/></Envelope>");
            var body = output.SelectSingleNode("//soap:Body", ns);

            // get the method name, select the first node in our custom namespace
            bool handled = false;
            if (input.SelectSingleNode("//x:*", ns) is XmlElement requestElement)
            {
                var responseElement = output.CreateElement(requestElement.LocalName + "Response", NamespaceUri);
                body.AppendChild(responseElement);

                if (HandleSoapMethod(output, requestElement, responseElement))
                {
                    context.Response.ContentType = "application/soap+xml; charset=utf-8";
                    context.Response.StatusCode = (int)HttpStatusCode.OK;
                    var writer = new XmlTextWriter(outputStream, Encoding.UTF8);
                    output.WriteTo(writer);
                    writer.Flush();
                    handled = true;
                }
            }

            if (!handled)
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            }
        }

        public void Stop() => _listener.Stop();
        public virtual void Dispose() => _listener.Close();
    }
}
like image 199
Simon Mourier Avatar answered Sep 30 '22 12:09

Simon Mourier


Personally, I use ServiceStack to create both client and server

https://docs.servicestack.net/soap-support

Or SoapHttpClient nuget

https://github.com/pmorelli92/SoapHttpClient

Or my example from way back when

Is it possible that I can convert simple string to SOAP Message and send it?

The answer depends on what framework or libraries do you plan to use?

like image 32
Margus Avatar answered Sep 30 '22 11:09

Margus